diff --git a/package-lock.json b/package-lock.json index 3ea65d2b117..8afbb3cbceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@esri/arcgis-rest-feature-layer": "^3.4.3", "@esri/arcgis-rest-portal": "^3.5.0", "@esri/arcgis-rest-request": "^3.1.1", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^3.1.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", @@ -4993,10 +4994,24 @@ "tslib": "^1.10.0" } }, + "node_modules/@esri/arcgis-rest-service-admin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-service-admin/-/arcgis-rest-service-admin-3.7.0.tgz", + "integrity": "sha512-vPm+hyO8lPH9aO1PMgveGevKbiViXgbpsetG3BLruxrxEhT0B+QytWE/LFp1beZYILTrm6umZqacEi6T2KJ7Aw==", + "dependencies": { + "@esri/arcgis-rest-types": "^3.7.0", + "tslib": "^1.13.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-auth": "^3.0.0", + "@esri/arcgis-rest-portal": "^3.0.0", + "@esri/arcgis-rest-request": "^3.0.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==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.7.0.tgz", + "integrity": "sha512-KEgORx0HKHOrV4oMYOwmZ76N89WTNkbKb1z3UYJrOEaKVGRU3jisgQcuTXFqjJJe4ZApGQhxCzNgcaU067qdpA==" }, "node_modules/@esri/hub-common": { "resolved": "packages/common", @@ -22644,9 +22659,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 +22677,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 +22708,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 +22729,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", @@ -64966,7 +64987,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "14.11.0", + "version": "14.12.0", "license": "Apache-2.0", "dependencies": { "abab": "^2.0.5", @@ -64986,6 +65007,7 @@ "@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-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^2.15.0 || 3" } }, @@ -68688,10 +68710,19 @@ "tslib": "^1.10.0" } }, + "@esri/arcgis-rest-service-admin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-service-admin/-/arcgis-rest-service-admin-3.7.0.tgz", + "integrity": "sha512-vPm+hyO8lPH9aO1PMgveGevKbiViXgbpsetG3BLruxrxEhT0B+QytWE/LFp1beZYILTrm6umZqacEi6T2KJ7Aw==", + "requires": { + "@esri/arcgis-rest-types": "^3.7.0", + "tslib": "^1.13.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==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.7.0.tgz", + "integrity": "sha512-KEgORx0HKHOrV4oMYOwmZ76N89WTNkbKb1z3UYJrOEaKVGRU3jisgQcuTXFqjJJe4ZApGQhxCzNgcaU067qdpA==" }, "@esri/hub-common": { "version": "file:packages/common", @@ -83376,7 +83407,8 @@ "lodash._baseindexof": { "version": "3.1.0", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -83391,17 +83423,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 +83450,8 @@ "lodash._getnative": { "version": "3.9.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._root": { "version": "3.0.1", @@ -83432,7 +83468,8 @@ "lodash.restparam": { "version": "3.6.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash.union": { "version": "4.6.0", diff --git a/package.json b/package.json index c7ea6e94a1a..3e8f916a5e7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@esri/arcgis-rest-feature-layer": "^3.4.3", "@esri/arcgis-rest-portal": "^3.5.0", "@esri/arcgis-rest-request": "^3.1.1", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^3.1.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", diff --git a/packages/common/package.json b/packages/common/package.json index 544e4106f27..f2445f5087c 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,6 +25,7 @@ "@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-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^2.15.0 || 3" }, "devDependencies": { diff --git a/packages/common/src/content/_internal/ContentSchema.ts b/packages/common/src/content/_internal/ContentSchema.ts index fe34878222b..74fa5c5abc4 100644 --- a/packages/common/src/content/_internal/ContentSchema.ts +++ b/packages/common/src/content/_internal/ContentSchema.ts @@ -2,7 +2,10 @@ import { IConfigurationSchema } from "../../core"; import { HubItemEntitySchema } from "../../core/schemas/shared/HubItemEntitySchema"; export type ContentEditorType = (typeof ContentEditorTypes)[number]; -export const ContentEditorTypes = ["hub:content:edit"] as const; +export const ContentEditorTypes = [ + "hub:content:edit", + "hub:content:settings", +] as const; /** * defines the JSON schema for a Hub Content's editable fields @@ -14,5 +17,11 @@ export const ContentSchema: IConfigurationSchema = { licenseInfo: { type: "string", }, + serverExtractCapability: { + type: "boolean", + }, + hostedDownloads: { + type: "boolean", + }, }, } as IConfigurationSchema; diff --git a/packages/common/src/content/_internal/ContentUiSchemaEdit.ts b/packages/common/src/content/_internal/ContentUiSchemaEdit.ts index 2dfe78b30f4..81efd0194d2 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaEdit.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaEdit.ts @@ -1,10 +1,10 @@ -import { IUiSchema } from "../../core"; import { IArcGISContext } from "../../ArcGISContext"; -import { IHubContent } from "../../core/types"; import { getTagItems } from "../../core/schemas/internal/getTagItems"; import { getCategoryItems } from "../../core/schemas/internal/getCategoryItems"; import { getLocationExtent } from "../../core/schemas/internal/getLocationExtent"; import { getLocationOptions } from "../../core/schemas/internal/getLocationOptions"; +import { IHubEditableContent } from "../../core/types/IHubEditableContent"; +import { IUiSchema } from "../../core/schemas/types"; /** * @private @@ -14,7 +14,7 @@ import { getLocationOptions } from "../../core/schemas/internal/getLocationOptio */ export const buildUiSchema = async ( i18nScope: string, - entity: IHubContent, + entity: IHubEditableContent, context: IArcGISContext ): Promise => { return { diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts new file mode 100644 index 00000000000..35462f84159 --- /dev/null +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -0,0 +1,65 @@ +import { IArcGISContext } from "../../ArcGISContext"; +import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; +import { IHubEditableContent } from "../../core/types/IHubEditableContent"; +import { isHostedFeatureServiceEntity } from "../hostedServiceUtils"; + +/** + * @private + * constructs the complete settings uiSchema for Hub Editable Content. + * This defines how the schema properties should be + * rendered in the content settings editing experience + */ +export const buildUiSchema = async ( + i18nScope: string, + entity: IHubEditableContent, + _context: IArcGISContext +): Promise => { + const uiSchema: IUiSchema = { + type: "Layout", + elements: [], + }; + if (isHostedFeatureServiceEntity(entity)) { + uiSchema.elements.push({ + type: "Section", + labelKey: `${i18nScope}.sections.downloads.label`, + options: { + helperText: { + labelKey: `${i18nScope}.sections.downloads.helperText`, + }, + }, + elements: [ + { + labelKey: `${i18nScope}.fields.serverExtractCapability.label`, + scope: "/properties/serverExtractCapability", + type: "Control", + options: { + helperText: { + labelKey: `${i18nScope}.fields.serverExtractCapability.helperText`, + }, + }, + }, + { + labelKey: `${i18nScope}.fields.hostedDownloads.label`, + scope: "/properties/hostedDownloads", + type: "Control", + options: { + helperText: { + labelKey: `${i18nScope}.fields.hostedDownloads.helperText`, + }, + }, + rule: { + effect: UiSchemaRuleEffects.DISABLE, + condition: { + schema: { + properties: { + serverExtractCapability: { const: false }, + }, + }, + }, + }, + }, + ], + }); + } + return uiSchema; +}; diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index 07fa1c7d09e..b3642d526e1 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -10,6 +10,11 @@ import { getHubRelativeUrl } from "./internalContentUtils"; import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; +import { + hasServiceCapability, + ServiceCapabilities, +} from "../hostedServiceUtils"; +import { IItemAndIServerEnrichments } from "../../items/_enrichments"; // if called and valid, set 3 things -- else just return type custom export const getItemExtent = (itemExtent: number[][]): IExtent => { @@ -36,7 +41,8 @@ export function deriveLocationFromItemExtent(itemExtent?: number[][]) { export function computeProps( model: IModel, content: Partial, - requestOptions: IRequestOptions + requestOptions: IRequestOptions, + enrichments: IItemAndIServerEnrichments = {} ): IHubEditableContent { let token: string; if (requestOptions.authentication) { @@ -70,5 +76,12 @@ export function computeProps( : deriveLocationFromItemExtent(model.item.extent); } + if (enrichments.server) { + content.serverExtractCapability = hasServiceCapability( + ServiceCapabilities.EXTRACT, + enrichments.server + ); + } + return content as IHubEditableContent; } diff --git a/packages/common/src/content/_internal/getPropertyMap.ts b/packages/common/src/content/_internal/getPropertyMap.ts index 63a0a0c6f19..bbe92e847c8 100644 --- a/packages/common/src/content/_internal/getPropertyMap.ts +++ b/packages/common/src/content/_internal/getPropertyMap.ts @@ -30,6 +30,11 @@ export function getPropertyMap(): IPropertyMap[] { storeKey: "item.licenseInfo", }); + map.push({ + entityKey: "hostedDownloads", + storeKey: "item.properties.downloads.hosted", + }); + // features is intentionally left out // TODO: look into composeContent() for what we can add here diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 3b566ddabe5..fda1a3f3bf0 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -10,7 +10,6 @@ import { IHubContentEditor, IHubEditableContent } from "../core"; // Note - we separate these imports so we can cleanly spy on things in tests import { createModel, - getModel, updateModel, // upsertModelResources, } from "../models"; @@ -19,8 +18,17 @@ import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { cloneObject } from "../util"; import { IModel } from "../types"; -import { computeProps } from "./_internal/computeProps"; import { getProp } from "../objects/get-prop"; +import { modelToHubEditableContent } from "./fetch"; +import { getService, parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; +import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; +import { + hasServiceCapability, + isHostedFeatureServiceEntity, + ServiceCapabilities, + toggleServiceCapability, +} from "./hostedServiceUtils"; +import { IItemAndIServerEnrichments } from "../items/_enrichments"; // TODO: move this to defaults? const DEFAULT_CONTENT_MODEL: IModel = { @@ -90,13 +98,11 @@ export async function updateContent( content: IHubEditableContent, requestOptions: IUserRequestOptions ): Promise { - // let resources; - - // get the backing item & data - // We can't just call getModel because we need to be able + // Get the backing item + // NOTE: We can't just call `getModel` because we need to be able // to properly handle other types like PDFs that don't have JSON data const item = await getItem(content.id, requestOptions); - const model = { item }; + const model: IModel = { item }; // create the PropertyMapper const mapper = new PropertyMapper, IModel>( getPropertyMap() @@ -111,30 +117,37 @@ export async function updateContent( modelToUpdate.item.properties.boundary = locationType === "none" ? "none" : "item"; - // TODO: if we have resources disconnect them from the model for now. - // if (modelToUpdate.resources) { - // resources = configureBaseResources( - // cloneObject(modelToUpdate.resources), - // EntityResourceMap - // ); - // delete modelToUpdate.resources; - // } // update the backing item const updatedModel = await updateModel(modelToUpdate, requestOptions); - // // if we have resources, create them, then re-attach them to the model - // if (resources) { - // updatedModel = await upsertModelResources( - // updatedModel, - // resources, - // requestOptions - // ); - // } - // now map back into a project and return that - let updatedContent = mapper.storeToEntity(updatedModel, content); - updatedContent = computeProps(model, updatedContent, requestOptions); - // the casting is needed because modelToObject returns a `Partial` - // where as this function returns a `T` - return updatedContent as IHubEditableContent; + + // update enrichment values + const enrichments: IItemAndIServerEnrichments = {}; + if (isHostedFeatureServiceEntity(content)) { + const currentDefinition = await getService({ + ...requestOptions, + url: content.url, + }); + const currentServerExtractEnabled = hasServiceCapability( + ServiceCapabilities.EXTRACT, + currentDefinition + ); + // To avoid over-updating the service, we only fire an update call if Extract has changed + if (currentServerExtractEnabled !== content.serverExtractCapability) { + const updatedDefinition = toggleServiceCapability( + ServiceCapabilities.EXTRACT, + currentDefinition + ); + await updateServiceDefinition(parseServiceUrl(content.url), { + authentication: requestOptions.authentication, + updateDefinition: updatedDefinition, + }); + enrichments.server = updatedDefinition; + } else { + enrichments.server = currentDefinition; + } + } + + return modelToHubEditableContent(updatedModel, requestOptions, enrichments); } /** diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index cf7d5056031..c311932e5d6 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -1,5 +1,6 @@ import { getLayer, + getService, parseServiceUrl, queryFeatures, } from "@esri/arcgis-rest-feature-layer"; @@ -9,6 +10,7 @@ import { ItemOrServerEnrichment, fetchItemEnrichments, IItemAndEnrichments, + IItemAndIServerEnrichments, } from "../items/_enrichments"; import { IHubRequestOptions, IModel } from "../types"; import { isNil } from "../util"; @@ -20,16 +22,12 @@ import { getContentEnrichments, } from "./_fetch"; import { canUseHubApiForItem } from "./_internal/internalContentUtils"; -import { - composeContent, - getItemLayer, - getProxyUrl, - isLayerView, -} from "./compose"; +import { composeContent, getItemLayer, getProxyUrl } from "./compose"; import { IRequestOptions } from "@esri/arcgis-rest-request"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; +import { isHostedFeatureServiceItem } from "./hostedServiceUtils"; const hasFeatures = (contentType: string) => ["Feature Layer", "Table"].includes(contentType); @@ -238,19 +236,28 @@ export const fetchHubContent = async ( identifier: string, requestOptions: IRequestOptions ): Promise => { - // TODO: fetch data? org? ownerUser? metadata? - // could use getItemEnrichments() and remove server, layers, etc - // but we'd have to do that _after_ fetching the item first - const enrichments = [] as ItemOrServerEnrichment[]; - const options = { ...requestOptions, enrichments } as IFetchContentOptions; - // for now we call fetchContent(), which returns a superset of IHubEditableContent - // in the long run we probably want to replace this w/ fetchItemAndEnrichments() - // and then use the property mapper and computeProps() to compose the object - const model = await fetchContent(identifier, options); - // for now we still need property mapper to get defaults not set by composeContent() + // NOTE: We can't just call `getModel` because we need to be able + // to properly handle other types like PDFs that don't have JSON data + const item = await getItem(identifier, requestOptions); + const enrichments: IItemAndIServerEnrichments = {}; + if (isHostedFeatureServiceItem(item)) { + enrichments.server = await getService({ + ...requestOptions, + url: parseServiceUrl(item.url), + }); + } + const model: IModel = { item }; + return modelToHubEditableContent(model, requestOptions, enrichments); +}; + +export function modelToHubEditableContent( + model: IModel, + requestOptions: IRequestOptions, + enrichments: IItemAndIServerEnrichments +) { const mapper = new PropertyMapper, IModel>( getPropertyMap() ); const content = mapper.storeToEntity(model, {}) as IHubEditableContent; - return computeProps(model, content, requestOptions); -}; + return computeProps(model, content, requestOptions, enrichments); +} diff --git a/packages/common/src/content/hostedServiceUtils.ts b/packages/common/src/content/hostedServiceUtils.ts new file mode 100644 index 00000000000..e2796700e7b --- /dev/null +++ b/packages/common/src/content/hostedServiceUtils.ts @@ -0,0 +1,118 @@ +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-feature-layer"; +import { IItem } from "@esri/arcgis-rest-portal"; +import { IHubEditableContent } from "../core/types/IHubEditableContent"; + +/** + * Determines whether an item represents a hosted feature service + * @param item item to check + * @returns whether the item represents a hosted feature service + */ +export function isHostedFeatureServiceItem(item: IItem): boolean { + return isHostedFeatureService(item.type, item.typeKeywords); +} + +/** + * Determines whether an entity represents a hosted feature service + * @param item item to check + * @returns whether the item represents a hosted feature service + */ +export function isHostedFeatureServiceEntity( + content: IHubEditableContent +): boolean { + return isHostedFeatureService(content.type, content.typeKeywords); +} + +/** + * @private + * base helper to determine whether the arguments correspond to a hosted feature service + * @param type an item type + * @param typeKeywords an item typeKeywords array + * @returns whether the arguments correspond to a hosted feature service + */ +function isHostedFeatureService( + type: string, + typeKeywords: string[] = [] +): boolean { + // This logic was given to us by the ArcGIS Online home app team. Apparently this is + // part of the check they internally do when deciding whether to show the "Export Data" + // button on the item settings page. See the "Tech" section of this issue for more details: + // https://devtopia.esri.com/dc/hub/issues/7210 + return type === "Feature Service" && typeKeywords.includes("Hosted Service"); +} + +export enum ServiceCapabilities { + EXTRACT = "Extract", +} + +/** + * Returns a whether a service has a capability + * @param capability + * @param serviceDefinition + * + * @returns {boolean} + */ +export function hasServiceCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +) { + const capabilities = (serviceDefinition.capabilities || "").split(","); + return capabilities.includes(capability); +} + +/** + * Toggles a single capability on a given feature service. + * Returns a service definition object with updated capabilities + * @param capability capability to toggle + * @param serviceDefinition current definition of the service + * + * @returns updated definition + */ +export function toggleServiceCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const updatedDefinition = hasServiceCapability(capability, serviceDefinition) + ? removeServiceCapability(capability, serviceDefinition) + : addServiceCapability(capability, serviceDefinition); + + return updatedDefinition; +} + +/** + * @private + * adds a capability onto a service definition + * @param capability capability to add + * @param serviceDefinition the definition to modify + * @returns a copy of the modified definition + */ +function addServiceCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const capabilities = (serviceDefinition.capabilities || "") + .split(",") + .filter((c) => !!c) + .concat(capability) + .join(","); + const updated = { ...serviceDefinition, capabilities }; + return updated; +} + +/** + * @private + * removes a capability from a service definition + * @param capability capability to remove + * @param serviceDefinition the definition to modify + * @returns a copy of the modified definition + */ +function removeServiceCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const capabilities = serviceDefinition.capabilities + .split(",") + .filter((c) => c !== capability) + .join(","); + const updated = { ...serviceDefinition, capabilities }; + return updated; +} diff --git a/packages/common/src/content/index.ts b/packages/common/src/content/index.ts index ecc8875e532..ce9318942e0 100644 --- a/packages/common/src/content/index.ts +++ b/packages/common/src/content/index.ts @@ -9,3 +9,4 @@ export * from "./HubContent"; export * from "./search"; export * from "./slugs"; export * from "./types"; +export * from "./hostedServiceUtils"; diff --git a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts index bd9222332b9..b93076be7dc 100644 --- a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts +++ b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts @@ -12,8 +12,8 @@ import { GroupEditorType } from "../../../groups/_internal/GroupSchema"; import { ConfigurableEntity } from "./ConfigurableEntity"; import { IArcGISContext } from "../../../ArcGISContext"; import { - IHubContent, IHubDiscussion, + IHubEditableContent, IHubGroup, IHubInitiative, IHubPage, @@ -151,10 +151,12 @@ export async function getEntityEditorSchemas( const contentModule = await { "hub:content:edit": () => import("../../../content/_internal/ContentUiSchemaEdit"), + "hub:content:settings": () => + import("../../../content/_internal/ContentUiSchemaSettings"), }[type as ContentEditorType](); uiSchema = await contentModule.buildUiSchema( i18nScope, - entity as IHubContent, + entity as IHubEditableContent, context ); diff --git a/packages/common/src/core/types/IHubEditableContent.ts b/packages/common/src/core/types/IHubEditableContent.ts index 9ddc9b04dc6..077db16afe9 100644 --- a/packages/common/src/core/types/IHubEditableContent.ts +++ b/packages/common/src/core/types/IHubEditableContent.ts @@ -13,6 +13,19 @@ export interface IHubEditableContent * perhaps using Pick from IHubContent */ licenseInfo: string; + /** + * If the item represents a service, shows whether the service has the "Extract" + * capability enabled. This is a pre-requisite for Hosted Downloads to work. + */ + serverExtractCapability?: boolean; + /** + * Indicates whether an item has opted into the hosted downloads experience + * + * NOTE: even if an item has opted into the hosted downloads experience, only items + * that meet specific criteria will actually see the hosted experience on the live view + * (i.e., the item is a Hosted Feature Service with the Extract capability enabled). + */ + hostedDownloads?: boolean; } export type IHubContentEditor = Omit & { diff --git a/packages/common/src/items/_enrichments.ts b/packages/common/src/items/_enrichments.ts index 2def9728d30..359127be7f5 100644 --- a/packages/common/src/items/_enrichments.ts +++ b/packages/common/src/items/_enrichments.ts @@ -37,6 +37,8 @@ export type ItemOrServerEnrichment = | keyof IItemEnrichments | keyof IServerEnrichments; +export type IItemAndIServerEnrichments = IItemEnrichments & IServerEnrichments; + /** * Lazy load XML parsing library and parse metadata XML into JSON * @param metadataXml diff --git a/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts new file mode 100644 index 00000000000..b3b375f9f44 --- /dev/null +++ b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts @@ -0,0 +1,74 @@ +import { buildUiSchema } from "../../../src/content/_internal/ContentUiSchemaSettings"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; +import * as hostedServiceUtilsModule from "../../../src/content/hostedServiceUtils"; +import { UiSchemaRuleEffects } from "../../../src/core/schemas/types"; + +describe("buildUiSchema: content settings", () => { + it("includes download fields for hosted feature service entities", async () => { + spyOn( + hostedServiceUtilsModule, + "isHostedFeatureServiceEntity" + ).and.returnValue(true); + + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.downloads.label", + options: { + helperText: { + labelKey: "some.scope.sections.downloads.helperText", + }, + }, + elements: [ + { + labelKey: "some.scope.fields.serverExtractCapability.label", + scope: "/properties/serverExtractCapability", + type: "Control", + options: { + helperText: { + labelKey: + "some.scope.fields.serverExtractCapability.helperText", + }, + }, + }, + { + labelKey: "some.scope.fields.hostedDownloads.label", + scope: "/properties/hostedDownloads", + type: "Control", + options: { + helperText: { + labelKey: "some.scope.fields.hostedDownloads.helperText", + }, + }, + rule: { + effect: UiSchemaRuleEffects.DISABLE, + condition: { + schema: { + properties: { + serverExtractCapability: { const: false }, + }, + }, + }, + }, + }, + ], + }, + ], + }); + }); + it("excludes download fields for other entities", async () => { + spyOn( + hostedServiceUtilsModule, + "isHostedFeatureServiceEntity" + ).and.returnValue(false); + + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [], + }); + }); +}); diff --git a/packages/common/test/content/computeProps.test.ts b/packages/common/test/content/computeProps.test.ts index 08fa8f9959d..52735bfe50c 100644 --- a/packages/common/test/content/computeProps.test.ts +++ b/packages/common/test/content/computeProps.test.ts @@ -4,6 +4,7 @@ import { getItemExtent, } from "../../src/content/_internal/computeProps"; import { IHubEditableContent } from "../../src/core/types/IHubEditableContent"; +import { IItemAndIServerEnrichments } from "../../src/items/_enrichments"; import { IHubRequestOptions, IModel } from "../../src/types"; import { cloneObject } from "../../src/util"; import { MOCK_HUB_REQOPTS } from "../mocks/mock-auth"; @@ -14,7 +15,28 @@ describe("content computeProps", () => { requestOptions = cloneObject(MOCK_HUB_REQOPTS); }); - it("computeProps model boundary undefined", () => { + it("handles when properties are undefined", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + }, + // no boundary set + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + // no location set + }; + + const chk = computeProps(model, content, requestOptions); + + expect(chk.location?.type).toBe("custom"); + }); + + it("handles when boundary is undefined", () => { const model: IModel = { item: { type: "Feature Service", @@ -25,7 +47,6 @@ describe("content computeProps", () => { // nothing set in properties }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -39,7 +60,7 @@ describe("content computeProps", () => { expect(chk.location?.type).toBe("custom"); }); - it("computeProps boundary defined as none", () => { + it("handles when boundary defined as none", () => { const model: IModel = { item: { type: "Feature Service", @@ -50,7 +71,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -75,7 +95,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -100,7 +119,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -114,6 +132,49 @@ describe("content computeProps", () => { expect(chk.links.siteRelative).toBe("/maps/my-slug"); }); + + it("adds server based enrichments if available", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + properties: {}, + }, + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + }; + const enrichments: IItemAndIServerEnrichments = { + server: { capabilities: "Extract" }, + }; + + const chk = computeProps(model, content, requestOptions, enrichments); + expect(chk.serverExtractCapability).toBeTruthy(); + }); + + it("handles when authentication isn't defined", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + properties: {}, + }, + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + }; + const withoutAuth = cloneObject(requestOptions); + delete withoutAuth.authentication; + + const chk = computeProps(model, content, withoutAuth); + expect(chk.thumbnail).toBeUndefined(); + }); }); describe("getItemExtent", () => { diff --git a/packages/common/test/content/edit.test.ts b/packages/common/test/content/edit.test.ts index f13a464294d..9490920e320 100644 --- a/packages/common/test/content/edit.test.ts +++ b/packages/common/test/content/edit.test.ts @@ -1,7 +1,9 @@ import * as portalModule from "@esri/arcgis-rest-portal"; +import * as featureLayerModule from "@esri/arcgis-rest-feature-layer"; +import * as adminModule from "@esri/arcgis-rest-service-admin"; import { MOCK_AUTH } from "../mocks/mock-auth"; import * as modelUtils from "../../src/models"; -import { IHubRequestOptions, IModel } from "../../src/types"; +import { IModel } from "../../src/types"; import { IHubEditableContent } from "../../src/core/types"; import { createContent, @@ -38,19 +40,38 @@ describe("content editing:", () => { }); }); describe("update content:", () => { - it("converts to a model and updates the item", async () => { - const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + let getItemSpy: jasmine.Spy; + let getServiceSpy: jasmine.Spy; + let updateModelSpy: jasmine.Spy; + let updateServiceSpy: jasmine.Spy; + beforeEach(() => { + getItemSpy = spyOn(portalModule, "getItem").and.returnValue( Promise.resolve({ item: { typeKeywords: [], }, }) ); - const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( + getServiceSpy = spyOn(featureLayerModule, "getService"); + updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( (m: IModel) => { return Promise.resolve(m); } ); + updateServiceSpy = spyOn( + adminModule, + "updateServiceDefinition" + ).and.callFake((_url: string, opts: any) => + Promise.resolve(opts.updateDefinition) + ); + }); + afterEach(() => { + getItemSpy.calls.reset(); + getServiceSpy.calls.reset(); + updateModelSpy.calls.reset(); + updateServiceSpy.calls.reset(); + }); + it("converts to a model and updates the item", async () => { const content: IHubEditableContent = { itemControl: "edit", id: GUID, @@ -82,22 +103,11 @@ describe("content editing:", () => { const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; expect(modelToUpdate.item.description).toBe(content.description); expect(modelToUpdate.item.properties.boundary).toBe("none"); + // No service is associated with Hub Initiatives + expect(getServiceSpy).not.toHaveBeenCalled(); + expect(updateServiceSpy).not.toHaveBeenCalled(); }); - }); - describe("update content with location:", () => { - it("converts to a model and updates the item", async () => { - const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( - Promise.resolve({ - item: { - typeKeywords: [], - }, - }) - ); - const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( - (m: IModel) => { - return Promise.resolve(m); - } - ); + it("handles when a location is explicitly set", async () => { const content: IHubEditableContent = { itemControl: "edit", id: GUID, @@ -129,6 +139,96 @@ describe("content editing:", () => { const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; expect(modelToUpdate.item.description).toBe(content.description); expect(modelToUpdate.item.properties.boundary).toBe("item"); + // No service is associated with Hub Initiatives + expect(getServiceSpy).not.toHaveBeenCalled(); + expect(updateServiceSpy).not.toHaveBeenCalled(); + }); + it("doesn't update the hosted service if configurations haven't changed", async () => { + const currentDefinition: Partial = + { capabilities: "Extract" }; + getServiceSpy.and.returnValue(Promise.resolve(currentDefinition)); + + const content: IHubEditableContent = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + location: { type: "item" }, + licenseInfo: "", + url: "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer", + // Indicates that Extract should enabled on the service, + // Since it already is, nothing should change + serverExtractCapability: true, + }; + const chk = await updateContent(content, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + expect(getItemSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + expect(getServiceSpy).toHaveBeenCalledTimes(1); + expect(updateServiceSpy).not.toHaveBeenCalled(); + }); + it("updates the hosted service if configurations have changed", async () => { + const currentDefinition: Partial = + { capabilities: "Query" }; + getServiceSpy.and.returnValue(Promise.resolve(currentDefinition)); + + const content: IHubEditableContent = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + location: { type: "item" }, + licenseInfo: "", + url: "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer", + // Indicates that Extract should enabled on the service, + // Since it currently isn't, the service will be updated + serverExtractCapability: true, + }; + const chk = await updateContent(content, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + expect(getItemSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + expect(getServiceSpy).toHaveBeenCalledTimes(1); + expect(updateServiceSpy).toHaveBeenCalledTimes(1); + const [url, { updateDefinition }] = updateServiceSpy.calls.argsFor(0); + expect(url).toEqual( + "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer" + ); + expect(updateDefinition).toEqual({ capabilities: "Query,Extract" }); }); }); describe("delete content", () => { diff --git a/packages/common/test/content/fetch.test.ts b/packages/common/test/content/fetch.test.ts index 3bee8066b7a..d527ecd720b 100644 --- a/packages/common/test/content/fetch.test.ts +++ b/packages/common/test/content/fetch.test.ts @@ -13,6 +13,17 @@ import * as _enrichmentsModule from "../../src/items/_enrichments"; import * as _fetchModule from "../../src/content/_fetch"; import * as documentItem from "../mocks/items/document.json"; import * as multiLayerFeatureServiceItem from "../mocks/items/multi-layer-feature-service.json"; +import { + HOSTED_FEATURE_SERVICE_DEFINITION, + HOSTED_FEATURE_SERVICE_GUID, + HOSTED_FEATURE_SERVICE_ITEM, + HOSTED_FEATURE_SERVICE_URL, + NON_HOSTED_FEATURE_SERVICE_GUID, + NON_HOSTED_FEATURE_SERVICE_ITEM, + PDF_GUID, + PDF_ITEM, +} from "./fixtures"; +import { MOCK_AUTH } from "../mocks/mock-auth"; // mock the item enrichments that would be returned for a multi-layer service const getMultiLayerItemEnrichments = () => { @@ -656,20 +667,70 @@ describe("fetchContent", () => { }); describe("fetchHubContent", () => { - it("defers to fetchContent", async () => { - const spy = spyOn( - require("../../src/content/fetch"), - "fetchContent" - ).and.returnValue( - Promise.resolve({ - item: { - type: "Feature Service", - id: "9001", - }, - }) + it("gets hosted feature service", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(HOSTED_FEATURE_SERVICE_ITEM) + ); + const getServiceSpy = spyOn( + featureLayerModule, + "getService" + ).and.returnValue(HOSTED_FEATURE_SERVICE_DEFINITION); + + const chk = await fetchHubContent(HOSTED_FEATURE_SERVICE_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(HOSTED_FEATURE_SERVICE_GUID); + expect(chk.owner).toBe(HOSTED_FEATURE_SERVICE_ITEM.owner); + expect(chk.serverExtractCapability).toBeTruthy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(HOSTED_FEATURE_SERVICE_GUID); + expect(getServiceSpy.calls.count()).toBe(1); + expect(getServiceSpy.calls.argsFor(0)[0].url).toBe( + HOSTED_FEATURE_SERVICE_URL ); - const fakeRequestOptions = {}; - await fetchHubContent("123", fakeRequestOptions); - expect(spy).toHaveBeenCalled(); + }); + + it("gets non-hosted feature service", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(NON_HOSTED_FEATURE_SERVICE_ITEM) + ); + const getServiceSpy = spyOn(featureLayerModule, "getService"); + + const chk = await fetchHubContent(NON_HOSTED_FEATURE_SERVICE_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(NON_HOSTED_FEATURE_SERVICE_GUID); + expect(chk.owner).toBe(NON_HOSTED_FEATURE_SERVICE_ITEM.owner); + expect(chk.serverExtractCapability).toBeFalsy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe( + NON_HOSTED_FEATURE_SERVICE_GUID + ); + // Service definition isn't fetched for non-hosted feature services + expect(getServiceSpy.calls.count()).toBe(0); + }); + + it("gets non-service content", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(PDF_ITEM) + ); + const getServiceSpy = spyOn(featureLayerModule, "getService"); + + const chk = await fetchHubContent(PDF_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(PDF_GUID); + expect(chk.owner).toBe(PDF_ITEM.owner); + expect(chk.serverExtractCapability).toBeFalsy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(PDF_GUID); + // Service definition isn't fetched items that aren't hosted feature services + expect(getServiceSpy.calls.count()).toBe(0); }); }); diff --git a/packages/common/test/content/fixtures.ts b/packages/common/test/content/fixtures.ts new file mode 100644 index 00000000000..57d61cb82ea --- /dev/null +++ b/packages/common/test/content/fixtures.ts @@ -0,0 +1,65 @@ +import { IItem } from "@esri/arcgis-rest-portal"; +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; + +export const HOSTED_FEATURE_SERVICE_GUID = "9001"; +export const HOSTED_FEATURE_SERVICE_URL = + "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer"; +export const HOSTED_FEATURE_SERVICE_ITEM: IItem = { + id: HOSTED_FEATURE_SERVICE_GUID, + access: "public", + owner: "dev_pre_hub_admin", + created: 1652819949000, + modified: 1652819949000, + description: "This is a mock description", + snippet: "this is a content snippet", + isOrgItem: true, + title: "Mock Content", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + tags: [], + categories: ["Basemap imagery", "Creative maps"], + thumbnail: "thumbnail/mock-thumbnail.png", + extent: [], + licenseInfo: "CC-BY-SA", + culture: "en-us", + contentOrigin: "self", + numViews: 10, + properties: {}, + size: 0, + url: HOSTED_FEATURE_SERVICE_URL, +}; +export const HOSTED_FEATURE_SERVICE_DEFINITION = { + capabilities: "Extract,Query", +} as IFeatureServiceDefinition; + +export const NON_HOSTED_FEATURE_SERVICE_GUID = "9002"; +export const NON_HOSTED_FEATURE_SERVICE_ITEM: IItem = { + ...HOSTED_FEATURE_SERVICE_ITEM, + id: NON_HOSTED_FEATURE_SERVICE_GUID, + typeKeywords: [], +}; + +export const PDF_GUID = "9003"; +export const PDF_ITEM: IItem = { + id: PDF_GUID, + access: "public", + owner: "dev_pre_hub_admin", + created: 1652819949000, + modified: 1652819949000, + description: "This is a mock description", + snippet: "this is a content snippet", + isOrgItem: true, + title: "Mock Content", + type: "PDF", + typeKeywords: [], + tags: [], + categories: [], + thumbnail: "thumbnail/mock-thumbnail.png", + extent: [], + licenseInfo: "CC-BY-SA", + culture: "en-us", + contentOrigin: "self", + numViews: 10, + properties: {}, + size: 1001, +}; diff --git a/packages/common/test/content/hostedServiceUtils.test.ts b/packages/common/test/content/hostedServiceUtils.test.ts new file mode 100644 index 00000000000..ee6b359ac8e --- /dev/null +++ b/packages/common/test/content/hostedServiceUtils.test.ts @@ -0,0 +1,86 @@ +import { IItem } from "@esri/arcgis-rest-portal"; +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; +import { IHubEditableContent } from "../../src"; +import { + hasServiceCapability, + isHostedFeatureServiceEntity, + isHostedFeatureServiceItem, + ServiceCapabilities, + toggleServiceCapability, +} from "../../src/content/hostedServiceUtils"; + +describe("isHostedFeatureServiceItem", () => { + it("returns true for hosted feature service items", () => { + const item = { + type: "Feature Service", + typeKeywords: ["Hosted Service"], + } as IItem; + + expect(isHostedFeatureServiceItem(item)).toBeTruthy(); + }); + + it("returns false for other items", () => { + const item = { type: "PDF" } as IItem; + expect(isHostedFeatureServiceItem(item)).toBeFalsy(); + }); +}); + +describe("isHostedFeatureServiceEntity", () => { + it("returns true for hosted feature service content entities", () => { + const entity = { + type: "Feature Service", + typeKeywords: ["Hosted Service"], + } as IHubEditableContent; + + expect(isHostedFeatureServiceEntity(entity)).toBeTruthy(); + }); + + it("returns false for other content entities", () => { + const entity = { type: "PDF" } as IHubEditableContent; + expect(isHostedFeatureServiceEntity(entity)).toBeFalsy(); + }); +}); + +describe("hasServiceCapability", () => { + it("returns false when no capabilities are defined", () => { + const result = hasServiceCapability( + ServiceCapabilities.EXTRACT, + {} as Partial + ); + expect(result).toBeFalsy(); + }); + it("returns false when capability is not included in the list", () => { + const result = hasServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query", + } as Partial); + expect(result).toBeFalsy(); + }); + it("returns true when capability is included in the list", () => { + const result = hasServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query,Extract", + } as Partial); + expect(result).toBeTruthy(); + }); +}); + +describe("toggleServiceCapability", () => { + it("turns capability on if none are defined", () => { + const result = toggleServiceCapability( + ServiceCapabilities.EXTRACT, + {} as Partial + ); + expect(result.capabilities).toBe("Extract"); + }); + it("turns capability on if not present", () => { + const result = toggleServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query", + } as Partial); + expect(result.capabilities).toBe("Query,Extract"); + }); + it("turns capability off if present", () => { + const result = toggleServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query,Extract", + } as Partial); + expect(result.capabilities).toBe("Query"); + }); +}); diff --git a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts index f9f19e02c50..35c8fcb103d 100644 --- a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts +++ b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts @@ -19,6 +19,7 @@ import * as DiscussionBuildCreateUiSchema from "../../../../src/discussions/_int import { ContentEditorTypes } from "../../../../src/content/_internal/ContentSchema"; import * as ContentBuildEditUiSchema from "../../../../src/content/_internal/ContentUiSchemaEdit"; +import * as ContentBuildSettingsUiSchema from "../../../../src/content/_internal/ContentUiSchemaSettings"; import { PageEditorTypes } from "../../../../src/pages/_internal/PageSchema"; import * as PageBuildEditUiSchema from "../../../../src/pages/_internal/PageUiSchemaEdit"; @@ -42,6 +43,7 @@ describe("getEntityEditorSchemas: ", () => { { type: DiscussionEditorTypes[0], buildFn: DiscussionBuildEditUiSchema }, { type: DiscussionEditorTypes[1], buildFn: DiscussionBuildCreateUiSchema }, { type: ContentEditorTypes[0], buildFn: ContentBuildEditUiSchema }, + { type: ContentEditorTypes[1], buildFn: ContentBuildSettingsUiSchema }, { type: PageEditorTypes[0], buildFn: PageBuildEditUiSchema }, { type: GroupEditorTypes[0], buildFn: GroupBuildEditUiSchema }, { type: GroupEditorTypes[1], buildFn: GroupBuildSettingsUiSchema },