diff --git a/package-lock.json b/package-lock.json index af93d620f8c..03c9243e9a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22659,10 +22659,9 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseindexof": { "version": "3.1.0", - "dev": true, + "extraneous": true, "inBundle": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseuniq": { "version": "4.6.0", @@ -22677,24 +22676,21 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._bindcallback": { "version": "3.0.1", - "dev": true, + "extraneous": true, "inBundle": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._cacheindexof": { "version": "3.0.2", - "dev": true, + "extraneous": true, "inBundle": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._createcache": { "version": "3.1.2", - "dev": true, + "extraneous": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "lodash._getnative": "^3.0.0" } @@ -22708,10 +22704,9 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._getnative": { "version": "3.9.1", - "dev": true, + "extraneous": true, "inBundle": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._root": { "version": "3.0.1", @@ -22729,10 +22724,9 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.restparam": { "version": "3.6.1", - "dev": true, + "extraneous": true, "inBundle": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.union": { "version": "4.6.0", @@ -64987,7 +64981,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "14.22.0", + "version": "14.23.0", "license": "Apache-2.0", "dependencies": { "abab": "^2.0.5", @@ -83407,8 +83401,7 @@ "lodash._baseindexof": { "version": "3.1.0", "bundled": true, - "dev": true, - "peer": true + "extraneous": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -83423,20 +83416,17 @@ "lodash._bindcallback": { "version": "3.0.1", "bundled": true, - "dev": true, - "peer": true + "extraneous": true }, "lodash._cacheindexof": { "version": "3.0.2", "bundled": true, - "dev": true, - "peer": true + "extraneous": true }, "lodash._createcache": { "version": "3.1.2", "bundled": true, - "dev": true, - "peer": true, + "extraneous": true, "requires": { "lodash._getnative": "^3.0.0" } @@ -83450,8 +83440,7 @@ "lodash._getnative": { "version": "3.9.1", "bundled": true, - "dev": true, - "peer": true + "extraneous": true }, "lodash._root": { "version": "3.0.1", @@ -83468,8 +83457,7 @@ "lodash.restparam": { "version": "3.6.1", "bundled": true, - "dev": true, - "peer": true + "extraneous": true }, "lodash.union": { "version": "4.6.0", diff --git a/packages/common/src/content/_internal/ContentBusinessRules.ts b/packages/common/src/content/_internal/ContentBusinessRules.ts index 2ae89ee5247..bc71a43df79 100644 --- a/packages/common/src/content/_internal/ContentBusinessRules.ts +++ b/packages/common/src/content/_internal/ContentBusinessRules.ts @@ -21,6 +21,7 @@ export const ContentPermissions = [ "hub:content:workspace:overview", "hub:content:workspace:dashboard", "hub:content:workspace:details", + "hub:content:workspace:discussion", "hub:content:workspace:settings", "hub:content:manage", ] as const; @@ -67,6 +68,10 @@ export const ContentPermissionPolicies: IPermissionPolicy[] = [ permission: "hub:content:workspace:details", dependencies: ["hub:content:edit"], }, + { + permission: "hub:content:workspace:discussion", + dependencies: ["hub:content:edit"], + }, { permission: "hub:content:workspace:settings", dependencies: ["hub:content:edit"], diff --git a/packages/common/src/content/_internal/ContentSchema.ts b/packages/common/src/content/_internal/ContentSchema.ts index 74fa5c5abc4..8e907c5896d 100644 --- a/packages/common/src/content/_internal/ContentSchema.ts +++ b/packages/common/src/content/_internal/ContentSchema.ts @@ -5,6 +5,7 @@ export type ContentEditorType = (typeof ContentEditorTypes)[number]; export const ContentEditorTypes = [ "hub:content:edit", "hub:content:settings", + "hub:content:discussions", ] as const; /** diff --git a/packages/common/src/content/_internal/ContentUiSchemaDiscussions.ts b/packages/common/src/content/_internal/ContentUiSchemaDiscussions.ts new file mode 100644 index 00000000000..cf2611c43f7 --- /dev/null +++ b/packages/common/src/content/_internal/ContentUiSchemaDiscussions.ts @@ -0,0 +1,44 @@ +import { IHubEditableContent } from "../../core/types"; +import { IArcGISContext } from "../../ArcGISContext"; +import { IUiSchema } from "../../core/schemas/types"; + +/** + * @private + * settings uiSchema for Hub Discussions - this + * defines how the schema properties should be + * rendered in the Discussions settings experience + */ +export const buildUiSchema = async ( + i18nScope: string, + entity: IHubEditableContent, + context: IArcGISContext +): Promise => { + return { + type: "Layout", + elements: [ + { + type: "Section", + labelKey: `${i18nScope}.sections.discussions.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.discussable.label`, + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + `{{${i18nScope}.fields.discussable.enabled.label:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.discussable.enabled.description:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.description:translate}}`, + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }; +}; diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index b36d3ba6b59..f28bc4f1b32 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -10,6 +10,7 @@ import { getContentEditUrl, getHubRelativeUrl } from "./internalContentUtils"; import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; +import { isDiscussable } from "../../discussions"; import { hasServiceCapability, ServiceCapabilities, @@ -72,6 +73,8 @@ export function computeProps( : deriveLocationFromItemExtent(model.item.extent); } + content.isDiscussable = isDiscussable(content); + if (enrichments.server) { content.serverExtractCapability = hasServiceCapability( ServiceCapabilities.EXTRACT, diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 2bd0042a80e..446385e2604 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -19,6 +19,7 @@ import { getPropertyMap } from "./_internal/getPropertyMap"; import { cloneObject } from "../util"; import { IModel } from "../types"; import { getProp } from "../objects/get-prop"; +import { setDiscussableKeyword } from "../discussions"; import { modelToHubEditableContent } from "./fetch"; import { getService, parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; @@ -58,6 +59,11 @@ export async function createContent( // this expansion solves the typing somehow const content = { /* ...DEFAULT_PROJECT, */ ...partialContent }; + content.typeKeywords = setDiscussableKeyword( + content.typeKeywords, + content.isDiscussable + ); + // Map project object onto a default project Model const mapper = new PropertyMapper, IModel>( getPropertyMap() @@ -103,6 +109,10 @@ export async function updateContent( // to properly handle other types like PDFs that don't have JSON data const item = await getItem(content.id, requestOptions); const model: IModel = { item }; + content.typeKeywords = setDiscussableKeyword( + content.typeKeywords, + content.isDiscussable + ); // create the PropertyMapper const mapper = new PropertyMapper, IModel>( getPropertyMap() diff --git a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts index 1e5ab95a619..74d121f6f6d 100644 --- a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts +++ b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts @@ -57,6 +57,8 @@ export async function getEntityEditorSchemas( import("../../../sites/_internal/SiteUiSchemaCreate"), "hub:site:followers": () => import("../../../sites/_internal/SiteUiSchemaFollowers"), + "hub:site:discussions": () => + import("../../../sites/_internal/SiteUiSchemaDiscussions"), }[type as SiteEditorType](); uiSchema = await siteModule.buildUiSchema( i18nScope, @@ -77,6 +79,8 @@ export async function getEntityEditorSchemas( import("../../../discussions/_internal/DiscussionUiSchemaEdit"), "hub:discussion:create": () => import("../../../discussions/_internal/DiscussionUiSchemaCreate"), + "hub:discussion:settings": () => + import("../../../discussions/_internal/DiscussionUiSchemaSettings"), }[type as DiscussionEditorType](); uiSchema = await discussionModule.buildUiSchema( i18nScope, @@ -153,6 +157,8 @@ export async function getEntityEditorSchemas( const contentModule = await { "hub:content:edit": () => import("../../../content/_internal/ContentUiSchemaEdit"), + "hub:content:discussions": () => + import("../../../content/_internal/ContentUiSchemaDiscussions"), "hub:content:settings": () => import("../../../content/_internal/ContentUiSchemaSettings"), }[type as ContentEditorType](); @@ -174,6 +180,8 @@ export async function getEntityEditorSchemas( import("../../../groups/_internal/GroupUiSchemaEdit"), "hub:group:settings": () => import("../../../groups/_internal/GroupUiSchemaSettings"), + "hub:group:discussions": () => + import("../../../groups/_internal/GroupUiSchemaDiscussions"), }[type as GroupEditorType](); uiSchema = await groupModule.buildUiSchema( i18nScope, diff --git a/packages/common/src/core/types/IHubGroup.ts b/packages/common/src/core/types/IHubGroup.ts index b2f9595da13..fd1fc78aefd 100644 --- a/packages/common/src/core/types/IHubGroup.ts +++ b/packages/common/src/core/types/IHubGroup.ts @@ -99,6 +99,11 @@ export interface IHubGroup extends IHubEntityBase, IWithPermissions { */ tags?: string[]; + /** + * System configurable typekeywords + */ + typeKeywords?: string[]; + /** * Group thumbnail url (read-only) */ diff --git a/packages/common/src/core/types/IHubSite.ts b/packages/common/src/core/types/IHubSite.ts index 7d6d4ef53b3..49c2b60972b 100644 --- a/packages/common/src/core/types/IHubSite.ts +++ b/packages/common/src/core/types/IHubSite.ts @@ -81,4 +81,6 @@ export interface IHubSite legacyTeams: string[]; } -export type IHubSiteEditor = IHubItemEntityEditor & {}; +export type IHubSiteEditor = IHubItemEntityEditor & { + _discussions?: boolean; +}; diff --git a/packages/common/src/discussions/_internal/DiscussionSchema.ts b/packages/common/src/discussions/_internal/DiscussionSchema.ts index 6d1348c41ae..8a7a1b7b059 100644 --- a/packages/common/src/discussions/_internal/DiscussionSchema.ts +++ b/packages/common/src/discussions/_internal/DiscussionSchema.ts @@ -5,6 +5,7 @@ export type DiscussionEditorType = (typeof DiscussionEditorTypes)[number]; export const DiscussionEditorTypes = [ "hub:discussion:edit", "hub:discussion:create", + "hub:discussion:settings", ] as const; /** diff --git a/packages/common/src/discussions/_internal/DiscussionUiSchemaEdit.ts b/packages/common/src/discussions/_internal/DiscussionUiSchemaEdit.ts index 8a10705a9ee..dcd76b179e7 100644 --- a/packages/common/src/discussions/_internal/DiscussionUiSchemaEdit.ts +++ b/packages/common/src/discussions/_internal/DiscussionUiSchemaEdit.ts @@ -182,29 +182,6 @@ export const buildUiSchema = async ( }, ], }, - { - type: "Section", - labelKey: `${i18nScope}.sections.settings.label`, - elements: [ - { - labelKey: `${i18nScope}.fields.discussable.label`, - scope: "/properties/isDiscussable", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - `{{${i18nScope}.fields.discussable.enabled.label:translate}}`, - `{{${i18nScope}.fields.discussable.disabled.label:translate}}`, - ], - descriptions: [ - `{{${i18nScope}.fields.discussable.enabled.description:translate}}`, - `{{${i18nScope}.fields.discussable.disabled.description:translate}}`, - ], - icons: ["speech-bubbles", "circle-disallowed"], - }, - }, - ], - }, ], }; }; diff --git a/packages/common/src/discussions/_internal/DiscussionUiSchemaSettings.ts b/packages/common/src/discussions/_internal/DiscussionUiSchemaSettings.ts new file mode 100644 index 00000000000..d1d1c0ece64 --- /dev/null +++ b/packages/common/src/discussions/_internal/DiscussionUiSchemaSettings.ts @@ -0,0 +1,44 @@ +import { IHubDiscussion } from "../../core/types"; +import { IArcGISContext } from "../../ArcGISContext"; +import { IUiSchema } from "../../core/schemas/types"; + +/** + * @private + * settings uiSchema for Hub Discussions - this + * defines how the schema properties should be + * rendered in the Discussions settings experience + */ +export const buildUiSchema = async ( + i18nScope: string, + entity: IHubDiscussion, + context: IArcGISContext +): Promise => { + return { + type: "Layout", + elements: [ + { + type: "Section", + labelKey: `${i18nScope}.sections.settings.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.discussable.label`, + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + `{{${i18nScope}.fields.discussable.enabled.label:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.discussable.enabled.description:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.description:translate}}`, + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }; +}; diff --git a/packages/common/src/discussions/utils.ts b/packages/common/src/discussions/utils.ts index 91eade70a98..d194295084d 100644 --- a/packages/common/src/discussions/utils.ts +++ b/packages/common/src/discussions/utils.ts @@ -40,7 +40,7 @@ export function setDiscussableKeyword( typeKeywords: string[], discussable: boolean ): string[] { - const updatedTypeKeywords = typeKeywords.filter( + const updatedTypeKeywords = (typeKeywords || []).filter( (typeKeyword: string) => typeKeyword !== CANNOT_DISCUSS ); if (!discussable) { diff --git a/packages/common/src/groups/HubGroups.ts b/packages/common/src/groups/HubGroups.ts index 355f98bab82..610a1f1d17f 100644 --- a/packages/common/src/groups/HubGroups.ts +++ b/packages/common/src/groups/HubGroups.ts @@ -17,6 +17,7 @@ import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; import { DEFAULT_GROUP } from "./defaults"; import { convertHubGroupToGroup } from "./_internal/convertHubGroupToGroup"; import { convertGroupToHubGroup } from "./_internal/convertGroupToHubGroup"; +import { setDiscussableKeyword } from "../discussions"; import { IHubSearchResult } from "../search/types/IHubSearchResult"; import { computeLinks } from "./_internal/computeLinks"; @@ -100,6 +101,10 @@ export async function createHubGroup( ): Promise { // merge the incoming and default groups const hubGroup = { ...DEFAULT_GROUP, ...partialGroup } as IHubGroup; + hubGroup.typeKeywords = setDiscussableKeyword( + hubGroup.typeKeywords, + hubGroup.isDiscussable + ); const group = convertHubGroupToGroup(hubGroup); const opts = { group, @@ -135,6 +140,10 @@ export async function updateHubGroup( hubGroup: IHubGroup, requestOptions: IRequestOptions ): Promise { + hubGroup.typeKeywords = setDiscussableKeyword( + hubGroup.typeKeywords, + hubGroup.isDiscussable + ); const group = convertHubGroupToGroup(hubGroup); const opts = { group, diff --git a/packages/common/src/groups/_internal/GroupBusinessRules.ts b/packages/common/src/groups/_internal/GroupBusinessRules.ts index 527a0456273..a15d9c1092d 100644 --- a/packages/common/src/groups/_internal/GroupBusinessRules.ts +++ b/packages/common/src/groups/_internal/GroupBusinessRules.ts @@ -15,6 +15,7 @@ export const GroupPermissions = [ "hub:group:workspace:overview", "hub:group:workspace:dashboard", "hub:group:workspace:details", + "hub:group:workspace:discussion", "hub:group:workspace:settings", "hub:group:workspace:collaborators", "hub:group:workspace:content", @@ -64,6 +65,10 @@ export const GroupPermissionPolicies: IPermissionPolicy[] = [ permission: "hub:group:workspace:details", dependencies: ["hub:group:edit"], }, + { + permission: "hub:group:workspace:discussion", + dependencies: ["hub:group:edit"], + }, { permission: "hub:group:workspace:settings", dependencies: ["hub:group:edit"], diff --git a/packages/common/src/groups/_internal/GroupSchema.ts b/packages/common/src/groups/_internal/GroupSchema.ts index d477aa7a940..8f4fab7f303 100644 --- a/packages/common/src/groups/_internal/GroupSchema.ts +++ b/packages/common/src/groups/_internal/GroupSchema.ts @@ -1,6 +1,7 @@ import { IConfigurationSchema } from "../../core"; import { ENTITY_IMAGE_SCHEMA, + ENTITY_IS_DISCUSSABLE_SCHEMA, ENTITY_NAME_SCHEMA, ENTITY_SUMMARY_SCHEMA, } from "../../core/schemas/shared"; @@ -9,6 +10,7 @@ export type GroupEditorType = (typeof GroupEditorTypes)[number]; export const GroupEditorTypes = [ "hub:group:edit", "hub:group:settings", + "hub:group:discussions", ] as const; /** @@ -36,5 +38,6 @@ export const GroupSchema: IConfigurationSchema = { enum: [false, true], default: false, }, + isDiscussable: ENTITY_IS_DISCUSSABLE_SCHEMA, }, } as IConfigurationSchema; diff --git a/packages/common/src/groups/_internal/GroupUiSchemaDiscussions.ts b/packages/common/src/groups/_internal/GroupUiSchemaDiscussions.ts new file mode 100644 index 00000000000..0211c817ab2 --- /dev/null +++ b/packages/common/src/groups/_internal/GroupUiSchemaDiscussions.ts @@ -0,0 +1,43 @@ +import { IHubGroup } from "../../core/types"; +import { IArcGISContext } from "../../ArcGISContext"; +import { IUiSchema } from "../../core/schemas/types"; +/** + * @private + * settings uiSchema for Hub Discussions - this + * defines how the schema properties should be + * rendered in the Discussions settings experience + */ +export const buildUiSchema = async ( + i18nScope: string, + entity: IHubGroup, + context: IArcGISContext +): Promise => { + return { + type: "Layout", + elements: [ + { + type: "Section", + labelKey: `${i18nScope}.sections.discussions.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.discussable.label`, + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + `{{${i18nScope}.fields.discussable.enabled.label:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.discussable.enabled.description:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.description:translate}}`, + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }; +}; diff --git a/packages/common/src/groups/_internal/getPropertyMap.ts b/packages/common/src/groups/_internal/getPropertyMap.ts index e5a5465efd4..2093bef5f66 100644 --- a/packages/common/src/groups/_internal/getPropertyMap.ts +++ b/packages/common/src/groups/_internal/getPropertyMap.ts @@ -36,6 +36,7 @@ export function getPropertyMap(): IPropertyMap[] { "tags", "thumbnail", "thumbnailUrl", + "typeKeywords", "userMembership", ]; groupProps.forEach((entry) => { diff --git a/packages/common/src/groups/defaults.ts b/packages/common/src/groups/defaults.ts index 68125fae978..2cf6b4ec300 100644 --- a/packages/common/src/groups/defaults.ts +++ b/packages/common/src/groups/defaults.ts @@ -10,6 +10,7 @@ export const DEFAULT_GROUP: Partial = { name: "No title provided", access: "private", permissions: [], + typeKeywords: [], }; /** diff --git a/packages/common/src/search/hubSearch.ts b/packages/common/src/search/hubSearch.ts index f82b495d81c..38826ad0c78 100644 --- a/packages/common/src/search/hubSearch.ts +++ b/packages/common/src/search/hubSearch.ts @@ -11,7 +11,11 @@ import { getApi } from "./_internal/commonHelpers/getApi"; import { portalSearchGroupMembers } from "./_internal/portalSearchGroupMembers"; import { portalSearchItems } from "./_internal/portalSearchItems"; import { portalSearchGroups } from "./_internal/portalSearchGroups"; -import { searchPortalUsersLegacy, searchPortalUsers, searchCommunityUsers } from "./_internal/portalSearchUsers"; +import { + searchPortalUsersLegacy, + searchPortalUsers, + searchCommunityUsers, +} from "./_internal/portalSearchUsers"; import { hubSearchItems } from "./_internal/hubSearchItems"; import { hubSearchChannels } from "./_internal/hubSearchChannels"; diff --git a/packages/common/src/sites/HubSite.ts b/packages/common/src/sites/HubSite.ts index 49bb8d03fe3..4f34891b9e8 100644 --- a/packages/common/src/sites/HubSite.ts +++ b/packages/common/src/sites/HubSite.ts @@ -424,6 +424,8 @@ export class HubSite editor ); + editor._discussions = this.entity.features["hub:site:feature:discussions"]; + return editor; } @@ -470,6 +472,7 @@ export class HubSite entity.features = { ...entity.features, "hub:site:feature:follow": editor._followers?.showFollowAction, + "hub:site:feature:discussions": editor._discussions, }; // copy the location extent up one level diff --git a/packages/common/src/sites/_internal/SiteBusinessRules.ts b/packages/common/src/sites/_internal/SiteBusinessRules.ts index 586219b5a41..b8499c19609 100644 --- a/packages/common/src/sites/_internal/SiteBusinessRules.ts +++ b/packages/common/src/sites/_internal/SiteBusinessRules.ts @@ -8,6 +8,7 @@ export const SiteDefaultFeatures: IFeatureFlags = { "hub:site:content": true, "hub:site:discussions": false, "hub:site:feature:follow": true, + "hub:site:feature:discussions": true, }; /** @@ -25,6 +26,7 @@ export const SitePermissions = [ "hub:site:content", "hub:site:discussions", "hub:site:feature:follow", + "hub:site:feature:discussions", "hub:site:workspace:overview", "hub:site:workspace:dashboard", "hub:site:workspace:details", @@ -35,6 +37,7 @@ export const SitePermissions = [ "hub:site:workspace:followers", "hub:site:workspace:followers:member", "hub:site:workspace:followers:manager", + "hub:site:workspace:discussion", "hub:site:manage", ] as const; @@ -83,6 +86,11 @@ export const SitesPermissionPolicies: IPermissionPolicy[] = [ permission: "hub:site:discussions", dependencies: ["hub:site:view"], }, + { + permission: "hub:site:feature:discussions", + dependencies: ["hub:site:view"], + entityConfigurable: true, + }, { permission: "hub:site:feature:follow", dependencies: ["hub:site:view"], @@ -144,6 +152,10 @@ export const SitesPermissionPolicies: IPermissionPolicy[] = [ }, ], }, + { + permission: "hub:site:workspace:discussion", + dependencies: ["hub:site:edit"], + }, { permission: "hub:site:manage", dependencies: ["hub:site:edit"], diff --git a/packages/common/src/sites/_internal/SiteSchema.ts b/packages/common/src/sites/_internal/SiteSchema.ts index 58acfacad81..782ebb2f0fd 100644 --- a/packages/common/src/sites/_internal/SiteSchema.ts +++ b/packages/common/src/sites/_internal/SiteSchema.ts @@ -1,4 +1,5 @@ import { IConfigurationSchema } from "../../core"; +import { ENTITY_IS_DISCUSSABLE_SCHEMA } from "../../core/schemas/shared"; import { HubItemEntitySchema } from "../../core/schemas/shared/HubItemEntitySchema"; export type SiteEditorType = (typeof SiteEditorTypes)[number]; @@ -6,6 +7,7 @@ export const SiteEditorTypes = [ "hub:site:edit", "hub:site:create", "hub:site:followers", + "hub:site:discussions", ] as const; /** @@ -13,4 +15,8 @@ export const SiteEditorTypes = [ */ export const SiteSchema: IConfigurationSchema = { ...HubItemEntitySchema, + properties: { + ...HubItemEntitySchema.properties, + _discussions: ENTITY_IS_DISCUSSABLE_SCHEMA, + }, } as IConfigurationSchema; diff --git a/packages/common/src/sites/_internal/SiteUiSchemaDiscussions.ts b/packages/common/src/sites/_internal/SiteUiSchemaDiscussions.ts new file mode 100644 index 00000000000..2be613024d6 --- /dev/null +++ b/packages/common/src/sites/_internal/SiteUiSchemaDiscussions.ts @@ -0,0 +1,44 @@ +import { IHubSite } from "../../core/types"; +import { IArcGISContext } from "../../ArcGISContext"; +import { IUiSchema } from "../../core/schemas/types"; + +/** + * @private + * settings uiSchema for Hub Discussions - this + * defines how the schema properties should be + * rendered in the Discussions settings experience + */ +export const buildUiSchema = async ( + i18nScope: string, + entity: IHubSite, + context: IArcGISContext +): Promise => { + return { + type: "Layout", + elements: [ + { + type: "Section", + labelKey: `${i18nScope}.sections.discussions.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.discussable.label`, + scope: "/properties/_discussions", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + `{{${i18nScope}.fields.discussable.enabled.label:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.discussable.enabled.description:translate}}`, + `{{${i18nScope}.fields.discussable.disabled.description:translate}}`, + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }; +}; diff --git a/packages/common/src/sites/_internal/capabilities/capabilityToFeatureMap.ts b/packages/common/src/sites/_internal/capabilities/capabilityToFeatureMap.ts index 12378650314..382dbf6a8b9 100644 --- a/packages/common/src/sites/_internal/capabilities/capabilityToFeatureMap.ts +++ b/packages/common/src/sites/_internal/capabilities/capabilityToFeatureMap.ts @@ -29,4 +29,9 @@ export const capabilityToFeatureMap: ICapabilityToFeatureMap[] = [ feature: "hub:site:feature:follow", negate: true, }, + { + capability: "disableDiscussions", + feature: "hub:site:feature:discussions", + negate: true, + }, ]; diff --git a/packages/common/src/sites/_internal/capabilities/types.ts b/packages/common/src/sites/_internal/capabilities/types.ts index 92ff6a349be..457d750b08e 100644 --- a/packages/common/src/sites/_internal/capabilities/types.ts +++ b/packages/common/src/sites/_internal/capabilities/types.ts @@ -3,7 +3,7 @@ import { SitePermissions } from "../SiteBusinessRules"; /** * legacy site capabilities */ -export type LegacyCapability = "hideFollow"; +export type LegacyCapability = "hideFollow" | "disableDiscussions"; /** * representation of legacy site capabilities as diff --git a/packages/common/test/content/_internal/ContentUiSchemaDiscussions.test.ts b/packages/common/test/content/_internal/ContentUiSchemaDiscussions.test.ts new file mode 100644 index 00000000000..024342b1bac --- /dev/null +++ b/packages/common/test/content/_internal/ContentUiSchemaDiscussions.test.ts @@ -0,0 +1,36 @@ +import { buildUiSchema } from "../../../src/content/_internal/ContentUiSchemaDiscussions"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; + +describe("buildUiSchema: content discussions", () => { + it("returns the full content discussions uiSchema", async () => { + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.discussions.label", + elements: [ + { + labelKey: "some.scope.fields.discussable.label", + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + "{{some.scope.fields.discussable.enabled.label:translate}}", + "{{some.scope.fields.discussable.disabled.label:translate}}", + ], + descriptions: [ + "{{some.scope.fields.discussable.enabled.description:translate}}", + "{{some.scope.fields.discussable.disabled.description:translate}}", + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts index 183883bc668..f32e9fae548 100644 --- a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts +++ b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts @@ -13,14 +13,17 @@ import { SiteEditorTypes } from "../../../../src/sites/_internal/SiteSchema"; import * as SiteBuildEditUiSchema from "../../../../src/sites/_internal/SiteUiSchemaEdit"; import * as SiteBuildCreateUiSchema from "../../../../src/sites/_internal/SiteUiSchemaCreate"; import * as SiteBuildFollowersUiSchema from "../../../../src/sites/_internal/SiteUiSchemaFollowers"; +import * as SiteBuildDiscussionsUiSchema from "../../../../src/sites/_internal/SiteUiSchemaDiscussions"; import { DiscussionEditorTypes } from "../../../../src/discussions/_internal/DiscussionSchema"; import * as DiscussionBuildEditUiSchema from "../../../../src/discussions/_internal/DiscussionUiSchemaEdit"; import * as DiscussionBuildCreateUiSchema from "../../../../src/discussions/_internal/DiscussionUiSchemaCreate"; +import * as DiscussionBuildSettingsUiSchema from "../../../../src/discussions/_internal/DiscussionUiSchemaSettings"; import { ContentEditorTypes } from "../../../../src/content/_internal/ContentSchema"; import * as ContentBuildEditUiSchema from "../../../../src/content/_internal/ContentUiSchemaEdit"; import * as ContentBuildSettingsUiSchema from "../../../../src/content/_internal/ContentUiSchemaSettings"; +import * as ContentBuildDiscussionsUiSchema from "../../../../src/content/_internal/ContentUiSchemaDiscussions"; import { PageEditorTypes } from "../../../../src/pages/_internal/PageSchema"; import * as PageBuildEditUiSchema from "../../../../src/pages/_internal/PageUiSchemaEdit"; @@ -28,6 +31,7 @@ import * as PageBuildEditUiSchema from "../../../../src/pages/_internal/PageUiSc import { GroupEditorTypes } from "../../../../src/groups/_internal/GroupSchema"; import * as GroupBuildEditUiSchema from "../../../../src/groups/_internal/GroupUiSchemaEdit"; import * as GroupBuildSettingsUiSchema from "../../../../src/groups/_internal/GroupUiSchemaSettings"; +import * as GroupBuildDiscussionsUiSchema from "../../../../src/groups/_internal/GroupUiSchemaDiscussions"; describe("getEntityEditorSchemas: ", () => { let uiSchemaBuildFnSpy: any; @@ -42,13 +46,20 @@ describe("getEntityEditorSchemas: ", () => { { type: SiteEditorTypes[0], buildFn: SiteBuildEditUiSchema }, { type: SiteEditorTypes[1], buildFn: SiteBuildCreateUiSchema }, { type: SiteEditorTypes[2], buildFn: SiteBuildFollowersUiSchema }, + { type: SiteEditorTypes[3], buildFn: SiteBuildDiscussionsUiSchema }, { type: DiscussionEditorTypes[0], buildFn: DiscussionBuildEditUiSchema }, { type: DiscussionEditorTypes[1], buildFn: DiscussionBuildCreateUiSchema }, + { + type: DiscussionEditorTypes[2], + buildFn: DiscussionBuildSettingsUiSchema, + }, { type: ContentEditorTypes[0], buildFn: ContentBuildEditUiSchema }, { type: ContentEditorTypes[1], buildFn: ContentBuildSettingsUiSchema }, + { type: ContentEditorTypes[2], buildFn: ContentBuildDiscussionsUiSchema }, { type: PageEditorTypes[0], buildFn: PageBuildEditUiSchema }, { type: GroupEditorTypes[0], buildFn: GroupBuildEditUiSchema }, { type: GroupEditorTypes[1], buildFn: GroupBuildSettingsUiSchema }, + { type: GroupEditorTypes[2], buildFn: GroupBuildDiscussionsUiSchema }, ].forEach(async ({ type, buildFn }) => { it("returns a schema & uiSchema for a given entity and editor type", async () => { uiSchemaBuildFnSpy = spyOn(buildFn, "buildUiSchema").and.returnValue( @@ -60,7 +71,6 @@ describe("getEntityEditorSchemas: ", () => { {} as any, {} as any ); - expect(uiSchemaBuildFnSpy).toHaveBeenCalledTimes(1); expect(schema).toBeDefined(); expect(uiSchema).toBeDefined(); diff --git a/packages/common/test/discussions/_internal/DiscussionUiSchemaEdit.test.ts b/packages/common/test/discussions/_internal/DiscussionUiSchemaEdit.test.ts index 9c610198c84..025f4aecdaf 100644 --- a/packages/common/test/discussions/_internal/DiscussionUiSchemaEdit.test.ts +++ b/packages/common/test/discussions/_internal/DiscussionUiSchemaEdit.test.ts @@ -180,29 +180,6 @@ describe("buildUiSchema: discussion edit", () => { }, ], }, - { - type: "Section", - labelKey: "some.scope.sections.settings.label", - elements: [ - { - labelKey: "some.scope.fields.discussable.label", - scope: "/properties/isDiscussable", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - "{{some.scope.fields.discussable.enabled.label:translate}}", - "{{some.scope.fields.discussable.disabled.label:translate}}", - ], - descriptions: [ - "{{some.scope.fields.discussable.enabled.description:translate}}", - "{{some.scope.fields.discussable.disabled.description:translate}}", - ], - icons: ["speech-bubbles", "circle-disallowed"], - }, - }, - ], - }, ], }); }); diff --git a/packages/common/test/discussions/_internal/DiscussionUiSchemaSettings.test.ts b/packages/common/test/discussions/_internal/DiscussionUiSchemaSettings.test.ts new file mode 100644 index 00000000000..5c8df621a8b --- /dev/null +++ b/packages/common/test/discussions/_internal/DiscussionUiSchemaSettings.test.ts @@ -0,0 +1,36 @@ +import { buildUiSchema } from "../../../src/discussions/_internal/DiscussionUiSchemaSettings"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; + +describe("buildUiSchema: discussions settings", () => { + it("returns the full discussions settings uiSchema", async () => { + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.settings.label", + elements: [ + { + labelKey: "some.scope.fields.discussable.label", + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + "{{some.scope.fields.discussable.enabled.label:translate}}", + "{{some.scope.fields.discussable.disabled.label:translate}}", + ], + descriptions: [ + "{{some.scope.fields.discussable.enabled.description:translate}}", + "{{some.scope.fields.discussable.disabled.description:translate}}", + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/common/test/groups/_internal/GroupUiSchemaDiscussions.test.ts b/packages/common/test/groups/_internal/GroupUiSchemaDiscussions.test.ts new file mode 100644 index 00000000000..2b1667b13c4 --- /dev/null +++ b/packages/common/test/groups/_internal/GroupUiSchemaDiscussions.test.ts @@ -0,0 +1,36 @@ +import { buildUiSchema } from "../../../src/groups/_internal/GroupUiSchemaDiscussions"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; + +describe("buildUiSchema: group discussions", () => { + it("returns the full group discussions uiSchema", async () => { + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.discussions.label", + elements: [ + { + labelKey: "some.scope.fields.discussable.label", + scope: "/properties/isDiscussable", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + "{{some.scope.fields.discussable.enabled.label:translate}}", + "{{some.scope.fields.discussable.disabled.label:translate}}", + ], + descriptions: [ + "{{some.scope.fields.discussable.enabled.description:translate}}", + "{{some.scope.fields.discussable.disabled.description:translate}}", + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/common/test/sites/HubSite.test.ts b/packages/common/test/sites/HubSite.test.ts index a98216ea86c..7c3f3fcbbba 100644 --- a/packages/common/test/sites/HubSite.test.ts +++ b/packages/common/test/sites/HubSite.test.ts @@ -385,6 +385,7 @@ describe("HubSite Class:", () => { "hub:site:discussions": false, "hub:site:events": false, "hub:site:feature:follow": true, + "hub:site:feature:discussions": true, }, }, }, @@ -482,6 +483,7 @@ describe("HubSite Class:", () => { "hub:site:discussions": false, "hub:site:events": false, "hub:site:feature:follow": true, + "hub:site:feature:discussions": true, }, }, }, diff --git a/packages/common/test/sites/_internal/SiteuUiSchemaDiscussions.test.ts b/packages/common/test/sites/_internal/SiteuUiSchemaDiscussions.test.ts new file mode 100644 index 00000000000..d5a9a1e3f5e --- /dev/null +++ b/packages/common/test/sites/_internal/SiteuUiSchemaDiscussions.test.ts @@ -0,0 +1,36 @@ +import { buildUiSchema } from "../../../src/sites/_internal/SiteUiSchemaDiscussions"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; + +describe("buildUiSchema: site discussions", () => { + it("returns the full site discussions uiSchema", async () => { + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.discussions.label", + elements: [ + { + labelKey: "some.scope.fields.discussable.label", + scope: "/properties/_discussions", + type: "Control", + options: { + control: "hub-field-input-radio", + labels: [ + "{{some.scope.fields.discussable.enabled.label:translate}}", + "{{some.scope.fields.discussable.disabled.label:translate}}", + ], + descriptions: [ + "{{some.scope.fields.discussable.enabled.description:translate}}", + "{{some.scope.fields.discussable.disabled.description:translate}}", + ], + icons: ["speech-bubbles", "circle-disallowed"], + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/common/test/sites/_internal/capabilities/convertFeaturesToLegacyCapabilities.test.ts b/packages/common/test/sites/_internal/capabilities/convertFeaturesToLegacyCapabilities.test.ts index db101e60729..c3e1dd90528 100644 --- a/packages/common/test/sites/_internal/capabilities/convertFeaturesToLegacyCapabilities.test.ts +++ b/packages/common/test/sites/_internal/capabilities/convertFeaturesToLegacyCapabilities.test.ts @@ -11,6 +11,7 @@ describe("convertFeaturesToLegacyCapabilities", () => { features: { "hub:site:content": true, "hub:site:feature:follow": false, + "hub:site:feature:discussions": false, }, }, values: { @@ -37,7 +38,10 @@ describe("convertFeaturesToLegacyCapabilities", () => { currentModel ); - expect(chk.data?.values.capabilities).toEqual(["hideFollow"]); + expect(chk.data?.values.capabilities).toEqual([ + "hideFollow", + "disableDiscussions", + ]); }); it("removes relevant features from the site's legacy capabilities array", () => { @@ -48,6 +52,7 @@ describe("convertFeaturesToLegacyCapabilities", () => { features: { "hub:site:content": true, "hub:site:feature:follow": true, + "hub:site:feature:discussions": true, }, }, values: { @@ -64,7 +69,7 @@ describe("convertFeaturesToLegacyCapabilities", () => { }, }, values: { - capabilities: ["hideFollow"], + capabilities: ["hideFollow", "disableDiscussions"], }, }, } as IModel; diff --git a/packages/common/test/sites/_internal/capabilities/migrateLegacyCapabilitiesToFeatures.test.ts b/packages/common/test/sites/_internal/capabilities/migrateLegacyCapabilitiesToFeatures.test.ts index 1921ab42cab..0f18597421a 100644 --- a/packages/common/test/sites/_internal/capabilities/migrateLegacyCapabilitiesToFeatures.test.ts +++ b/packages/common/test/sites/_internal/capabilities/migrateLegacyCapabilitiesToFeatures.test.ts @@ -9,7 +9,7 @@ describe("migrateLegacyCapabilitiesToFeatures", () => { data: { settings: {}, values: { - capabilities: ["hideFollow"], + capabilities: ["hideFollow", "disableDiscussions"], }, }, } as IModel; @@ -18,6 +18,7 @@ describe("migrateLegacyCapabilitiesToFeatures", () => { expect(chk.data?.settings.features).toEqual({ "hub:site:feature:follow": false, + "hub:site:feature:discussions": false, }); }); it("add features if they are not present in the legacy capabilities array - e.g. they are false", () => { @@ -33,6 +34,7 @@ describe("migrateLegacyCapabilitiesToFeatures", () => { expect(chk.data?.settings.features).toEqual({ "hub:site:feature:follow": true, + "hub:site:feature:discussions": true, }); }); it("adds legacy capabilities to existing features on a site", () => { @@ -45,7 +47,7 @@ describe("migrateLegacyCapabilitiesToFeatures", () => { }, }, values: { - capabilities: ["hideFollow"], + capabilities: ["hideFollow", "disableDiscussions"], }, }, } as IModel; @@ -55,6 +57,7 @@ describe("migrateLegacyCapabilitiesToFeatures", () => { expect(chk.data?.settings.features).toEqual({ "hub:site:events": true, "hub:site:feature:follow": false, + "hub:site:feature:discussions": false, }); }); });