diff --git a/packages/common/src/core/types/IHubGroup.ts b/packages/common/src/core/types/IHubGroup.ts index e420477d273..e6f9de6745e 100644 --- a/packages/common/src/core/types/IHubGroup.ts +++ b/packages/common/src/core/types/IHubGroup.ts @@ -139,6 +139,12 @@ export interface IHubGroup /** Whether members of the group are hidden */ hiddenMembers?: boolean; + + leavingDisallowed?: boolean; + + isOpenData?: boolean; + + _join?: "invite" | "request" | "auto"; } /** diff --git a/packages/common/src/groups/_internal/GroupSchema.ts b/packages/common/src/groups/_internal/GroupSchema.ts index 5d4c5c5b519..bf3bfaff23a 100644 --- a/packages/common/src/groups/_internal/GroupSchema.ts +++ b/packages/common/src/groups/_internal/GroupSchema.ts @@ -36,21 +36,67 @@ export const GroupSchema: IConfigurationSchema = { }, description: { type: "string" }, _thumbnail: ENTITY_IMAGE_SCHEMA, + access: { + type: "string", + enum: ["private", "org", "public"], + default: "private", + }, isSharedUpdate: { type: "boolean", enum: [false, true], default: false }, + leavingDisallowed: { type: "boolean", enum: [false, true], default: false }, + isOpenData: { type: "boolean", enum: [false, true], default: false }, membershipAccess: { type: "string", enum: ["organization", "collaborators", "anyone"], - default: "anyone", + default: "organization", }, isViewOnly: { type: "boolean", enum: [false, true], default: false, }, + _join: { + type: "string", + enum: ["invite", "request", "auto"], + default: "invite", + }, + hiddenMembers: { + type: "boolean", + enum: [false, true], + default: false, + }, isDiscussable: ENTITY_IS_DISCUSSABLE_SCHEMA, }, allOf: [ - // if the group is a shared update group, it must have a membershipAccess of org or collaborators + // if the group is not public, isOpenData must be false + { + if: { + properties: { + access: { pattern: "(private|org)" }, + }, + }, + then: { + properties: { + isOpenData: { const: false }, + }, + }, + }, + // if the group is is an admin group (leavingDisallowed === true), it must have a membershipAccess of organization + { + if: { + properties: { + leavingDisallowed: { const: true }, + }, + }, + then: { + properties: { + membershipAccess: { + type: "string", + const: "organization", + }, + }, + }, + }, + // if the group is a shared update group (isSharedUpdate === true), it must have a membershipAccess of org or collaborators { if: { properties: { @@ -60,10 +106,46 @@ export const GroupSchema: IConfigurationSchema = { then: { properties: { membershipAccess: { + type: "string", pattern: "(organization|collaborators)", }, }, }, }, + // if the group has access === 'private', then the _join must be 'invite' + { + if: { + properties: { + access: { const: "private" }, + }, + }, + then: { + properties: { + _join: { const: "invite" }, + }, + }, + }, + // if the group is admin (leavingDisallowed === true) or isSharedUpdate === true, _join must be 'invite' or 'request' + { + if: { + anyOf: [ + { + properties: { + leavingDisallowed: { const: true }, + }, + }, + { + properties: { + isSharedUpdate: { const: true }, + }, + }, + ], + }, + then: { + properties: { + _join: { pattern: "(invite|request)" }, + }, + }, + }, ], } as IConfigurationSchema; diff --git a/packages/common/src/groups/_internal/GroupUiSchemaCreate.ts b/packages/common/src/groups/_internal/GroupUiSchemaCreate.ts index bec85dfffbd..6488ac6841e 100644 --- a/packages/common/src/groups/_internal/GroupUiSchemaCreate.ts +++ b/packages/common/src/groups/_internal/GroupUiSchemaCreate.ts @@ -7,6 +7,8 @@ import { IArcGISContext } from "../../ArcGISContext"; import { EntityEditorOptions } from "../../core/schemas/internal/EditorOptions"; import { checkPermission } from "../../permissions"; import { getWellKnownGroup } from "../getWellKnownGroup"; +import { IHubGroup } from "../../core"; +import { getProp } from "../../objects"; /** * @private @@ -16,117 +18,433 @@ import { getWellKnownGroup } from "../getWellKnownGroup"; */ export const buildUiSchema = async ( i18nScope: string, - options: EntityEditorOptions, + options: Partial, context: IArcGISContext ): Promise => { return { type: "Layout", elements: [ { - labelKey: `${i18nScope}.fields.name.label`, - scope: "/properties/name", - type: "Control", - options: { - messages: [ - { - type: "ERROR", - keyword: "required", - icon: true, - labelKey: `${i18nScope}.fields.name.requiredError`, + 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`, + }, + { + type: "ERROR", + keyword: "maxLength", + icon: true, + labelKey: `${i18nScope}.fields.name.maxLengthError`, + }, + ], }, - { - type: "ERROR", - keyword: "maxLength", - icon: true, - labelKey: `${i18nScope}.fields.name.maxLengthError`, + }, + { + labelKey: `${i18nScope}.fields.access.label`, + scope: "/properties/access", + type: "Control", + options: { + control: "hub-field-input-tile-select", + descriptions: [ + `{{${i18nScope}.fields.access.private.description:translate}}`, + `{{${i18nScope}.fields.access.org.description:translate}}`, + `{{${i18nScope}.fields.access.public.description:translate}}`, + ], + icons: ["users", "organization", "globe"], + labels: [ + `{{${i18nScope}.fields.access.private.label:translate}}`, + `{{${i18nScope}.fields.access.org.label:translate}}`, + `{{${i18nScope}.fields.access.public.label:translate}}`, + ], + rules: [ + { + effect: UiSchemaRuleEffects.NONE, + }, + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [ + checkPermission( + "platform:portal:user:shareGroupToOrg", + context + ).access, + ], + }, + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [ + checkPermission( + "platform:portal:user:shareGroupToPublic", + context + ).access, + ], + }, + ], }, - ], - }, - }, - { - labelKey: `${i18nScope}.fields.isSharedUpdate.label`, - scope: "/properties/isSharedUpdate", - type: "Control", - options: { - control: "hub-field-input-switch", - helperText: { - labelKey: `${i18nScope}.fields.isSharedUpdate.helperText`, }, - }, + ], }, { - labelKey: `${i18nScope}.fields.membershipAccess.label`, - scope: "/properties/membershipAccess", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - `{{${i18nScope}.fields.membershipAccess.org:translate}}`, - `{{${i18nScope}.fields.membershipAccess.collab:translate}}`, - `{{${i18nScope}.fields.membershipAccess.any:translate}}`, - ], - rules: [ - [ - // we should be able to use undefined in this position but when used with entityEditor, we run this through interpolateTranslations - // which filters out the undefined which makes this not work as expected - // so for now we add a rule that doesn't do anything - { - effect: UiSchemaRuleEffects.NONE, + type: "Section", + labelKey: `${i18nScope}.sections.capabilities.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.isSharedUpdate.label`, + scope: "/properties/isSharedUpdate", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `${i18nScope}.fields.isSharedUpdate.helperText`, }, - ], - [ + }, + rule: { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [ + checkPermission( + "platform:portal:admin:createUpdateCapableGroup", + context + ).access, + ], + }, + }, + { + labelKey: `${i18nScope}.fields.isAdmin.label`, + scope: "/properties/leavingDisallowed", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `${i18nScope}.fields.isAdmin.helperText`, + }, + }, + rules: [ { - effect: UiSchemaRuleEffects.DISABLE, + effect: UiSchemaRuleEffects.ENABLE, conditions: [ - !checkPermission( - "platform:portal:user:addExternalMembersToGroup", + checkPermission( + "platform:portal:admin:createLeavingDisallowedGroup", context ).access, ], }, ], - [ + }, + { + labelKey: `${i18nScope}.fields.isOpenData.label`, + scope: "/properties/isOpenData", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `${i18nScope}.fields.isOpenData.helperText`, + }, + messages: [ + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `${i18nScope}.fields.isOpenData.constError`, + }, + ], + }, + rules: [ { - effect: UiSchemaRuleEffects.DISABLE, + effect: UiSchemaRuleEffects.ENABLE, conditions: [ - !checkPermission( - "platform:portal:user:addExternalMembersToGroup", + { + scope: "/properties/access", + schema: { const: "public" }, + }, + checkPermission( + "platform:opendata:user:designateGroup", context ).access, ], }, { - effect: UiSchemaRuleEffects.DISABLE, + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/access", + schema: { not: { const: "public" } }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.SHOW, + conditions: [ + // should only exist if user's org has portal.portalProperties.opendata.enabled: true + !!getProp( + context, + "portal.portalProperties.openData.enabled" + ), + ], + }, + ], + }, + ], + }, + { + type: "Section", + labelKey: `${i18nScope}.sections.membershipAccess.label`, + elements: [ + { + labelKey: `${i18nScope}.fields.membershipAccess.label`, + scope: "/properties/membershipAccess", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{${i18nScope}.fields.membershipAccess.org.label:translate}}`, + `{{${i18nScope}.fields.membershipAccess.collab.label:translate}}`, + `{{${i18nScope}.fields.membershipAccess.any.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.membershipAccess.org.description:translate}}`, + `{{${i18nScope}.fields.membershipAccess.collab.description:translate}}`, + `{{${i18nScope}.fields.membershipAccess.any.description:translate}}`, + ], + // rules that pertain to the individual options + rules: [ + [ + { + effect: UiSchemaRuleEffects.NONE, + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + ], + }, + ], + ], + messages: [ + { + type: "ERROR", + keyword: "pattern", + icon: true, + labelKey: `${i18nScope}.fields.membershipAccess.patternError`, + }, + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `${i18nScope}.fields.membershipAccess.constError`, + }, + ], + }, + // rules that pertain to the control as a whole + rules: [ + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.RESET, conditions: [ { scope: "/properties/isSharedUpdate", schema: { const: true }, }, + { + scope: "/properties/membershipAccess", + schema: { const: "anyone" }, + }, ], }, ], - ], - messages: [ - { - type: "ERROR", - keyword: "enum", - icon: true, - labelKey: `${i18nScope}.fields.membershipAccess.enumError`, + }, + { + labelKey: `${i18nScope}.fields.join.label`, + scope: "/properties/_join", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{${i18nScope}.fields.join.invite.label:translate}}`, + `{{${i18nScope}.fields.join.request.label:translate}}`, + `{{${i18nScope}.fields.join.auto.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.join.invite.description:translate}}`, + `{{${i18nScope}.fields.join.request.description:translate}}`, + `{{${i18nScope}.fields.join.auto.description:translate}}`, + ], + // rules that pertain to the individual options + rules: [ + [ + { + effect: UiSchemaRuleEffects.NONE, + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + ], + }, + ], + ], + messages: [ + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `${i18nScope}.fields.join.constError`, + }, + { + type: "ERROR", + keyword: "pattern", + icon: true, + labelKey: `${i18nScope}.fields.join.patternError`, + }, + ], }, - ], - }, - }, - { - labelKey: `${i18nScope}.fields.contributeContent.label`, - scope: "/properties/isViewOnly", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - `{{${i18nScope}.fields.contributeContent.all:translate}}`, - `{{${i18nScope}.fields.contributeContent.admins:translate}}`, - ], - }, + // rules that pertain to the control as a whole + rules: [ + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + { + scope: "/properties/_join", + schema: { const: "auto" }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + { + scope: "/properties/_join", + schema: { const: "auto" }, + }, + ], + }, + ], + }, + { + labelKey: `${i18nScope}.fields.hiddenMembers.label`, + scope: "/properties/hiddenMembers", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{${i18nScope}.fields.hiddenMembers.members.label:translate}}`, + `{{${i18nScope}.fields.hiddenMembers.admins.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.hiddenMembers.members.description:translate}}`, + `{{${i18nScope}.fields.hiddenMembers.admins.description:translate}}`, + ], + }, + }, + { + labelKey: `${i18nScope}.fields.contributeContent.label`, + scope: "/properties/isViewOnly", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{${i18nScope}.fields.contributeContent.members.label:translate}}`, + `{{${i18nScope}.fields.contributeContent.admins.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.contributeContent.members.description:translate}}`, + `{{${i18nScope}.fields.contributeContent.admins.description:translate}}`, + ], + }, + }, + ], }, ], }; @@ -148,7 +466,7 @@ export const buildDefaults = async ( context: IArcGISContext ): Promise => { return { - ...getWellKnownGroup("hubViewGroup", context), - membershipAccess: "organization", + ...getWellKnownGroup("hubGroup", context), + ...options, }; }; diff --git a/packages/common/src/groups/_internal/convertHubGroupToGroup.ts b/packages/common/src/groups/_internal/convertHubGroupToGroup.ts index d45981a0103..0d50be1a082 100644 --- a/packages/common/src/groups/_internal/convertHubGroupToGroup.ts +++ b/packages/common/src/groups/_internal/convertHubGroupToGroup.ts @@ -9,6 +9,22 @@ import { getPropertyMap } from "./getPropertyMap"; */ export function convertHubGroupToGroup(hubGroup: IHubGroup): IGroup { + // take the _join props and map them to isInvitationOnly and autoJoin + switch (hubGroup._join) { + case "invite": + hubGroup.isInvitationOnly = true; + hubGroup.autoJoin = false; + break; + case "request": + hubGroup.isInvitationOnly = false; + hubGroup.autoJoin = false; + break; + case "auto": + hubGroup.isInvitationOnly = false; + hubGroup.autoJoin = true; + break; + } + const mapper = new PropertyMapper, IGroup>( getPropertyMap() ); diff --git a/packages/common/src/groups/_internal/getPropertyMap.ts b/packages/common/src/groups/_internal/getPropertyMap.ts index f861e2949fe..f7a93a82607 100644 --- a/packages/common/src/groups/_internal/getPropertyMap.ts +++ b/packages/common/src/groups/_internal/getPropertyMap.ts @@ -39,6 +39,9 @@ export function getPropertyMap(): IPropertyMap[] { "thumbnailUrl", "typeKeywords", "userMembership", + "isOpenData", + "hiddenMembers", + "leavingDisallowed", ]; groupProps.forEach((entry) => { map.push({ entityKey: entry, storeKey: entry }); diff --git a/packages/common/src/groups/getWellKnownGroup.ts b/packages/common/src/groups/getWellKnownGroup.ts index a02e04a3867..e9bb48b4f3e 100644 --- a/packages/common/src/groups/getWellKnownGroup.ts +++ b/packages/common/src/groups/getWellKnownGroup.ts @@ -3,6 +3,7 @@ import { checkPermission } from "../permissions"; import { IArcGISContext } from "../ArcGISContext"; type WellKnownGroup = + | "hubGroup" | "hubViewGroup" | "hubEditGroup" | "hubFollowersGroup" @@ -18,6 +19,16 @@ export function getWellKnownGroup( context: IArcGISContext ): Partial { const configs: Record> = { + hubGroup: { + access: "private", + autoJoin: false, + isSharedUpdate: false, + isInvitationOnly: false, + hiddenMembers: false, + isViewOnly: false, + tags: ["Hub Group"], + membershipAccess: "organization", + }, hubViewGroup: { access: "org", autoJoin: false, diff --git a/packages/common/src/permissions/PlatformPermissionPolicies.ts b/packages/common/src/permissions/PlatformPermissionPolicies.ts index ba05590b675..a3985883488 100644 --- a/packages/common/src/permissions/PlatformPermissionPolicies.ts +++ b/packages/common/src/permissions/PlatformPermissionPolicies.ts @@ -17,6 +17,7 @@ export const PlatformPermissions = [ "platform:portal:admin:changeUserRoles", "platform:portal:admin:createGPWebhook", "platform:portal:admin:createUpdateCapableGroup", + "platform:portal:admin:createLeavingDisallowedGroup", "platform:portal:admin:deleteGroups", "platform:portal:admin:deleteItems", "platform:portal:admin:deleteUsers", @@ -188,6 +189,12 @@ export const PlatformPermissionPolicies: IPermissionPolicy[] = [ authenticated: true, privileges: ["portal:admin:createUpdateCapableGroup"], }, + { + permission: "platform:portal:admin:createLeavingDisallowedGroup", + services: ["portal"], + authenticated: true, + privileges: ["portal:admin:createLeavingDisallowedGroup"], + }, { permission: "platform:portal:admin:deleteGroups", services: ["portal"], diff --git a/packages/common/src/permissions/types/PlatformPrivilege.ts b/packages/common/src/permissions/types/PlatformPrivilege.ts index 1f65dfc89f2..60f3e0ff24d 100644 --- a/packages/common/src/permissions/types/PlatformPrivilege.ts +++ b/packages/common/src/permissions/types/PlatformPrivilege.ts @@ -17,6 +17,7 @@ export type PlatformPrivilege = | "portal:admin:changeUserRoles" | "portal:admin:createGPWebhook" | "portal:admin:createUpdateCapableGroup" + | "portal:admin:createLeavingDisallowedGroup" | "portal:admin:deleteGroups" | "portal:admin:deleteItems" | "portal:admin:deleteUsers" diff --git a/packages/common/test/groups/_internal/GroupUiSchemaCreate.test.ts b/packages/common/test/groups/_internal/GroupUiSchemaCreate.test.ts index 4cdf0d662fc..0b529de81df 100644 --- a/packages/common/test/groups/_internal/GroupUiSchemaCreate.test.ts +++ b/packages/common/test/groups/_internal/GroupUiSchemaCreate.test.ts @@ -20,97 +20,397 @@ describe("GroupUiSchemaCreate", () => { type: "Layout", elements: [ { - labelKey: `some.scope.fields.name.label`, - scope: "/properties/name", - type: "Control", - options: { - messages: [ - { - type: "ERROR", - keyword: "required", - icon: true, - labelKey: `some.scope.fields.name.requiredError`, + type: "Section", + labelKey: `some.scope.sections.basicInfo.label`, + elements: [ + { + labelKey: `some.scope.fields.name.label`, + scope: "/properties/name", + type: "Control", + options: { + messages: [ + { + type: "ERROR", + keyword: "required", + icon: true, + labelKey: `some.scope.fields.name.requiredError`, + }, + { + type: "ERROR", + keyword: "maxLength", + icon: true, + labelKey: `some.scope.fields.name.maxLengthError`, + }, + ], }, - { - type: "ERROR", - keyword: "maxLength", - icon: true, - labelKey: `some.scope.fields.name.maxLengthError`, + }, + { + labelKey: `some.scope.fields.access.label`, + scope: "/properties/access", + type: "Control", + options: { + control: "hub-field-input-tile-select", + descriptions: [ + `{{some.scope.fields.access.private.description:translate}}`, + `{{some.scope.fields.access.org.description:translate}}`, + `{{some.scope.fields.access.public.description:translate}}`, + ], + icons: ["users", "organization", "globe"], + labels: [ + `{{some.scope.fields.access.private.label:translate}}`, + `{{some.scope.fields.access.org.label:translate}}`, + `{{some.scope.fields.access.public.label:translate}}`, + ], + rules: [ + { + effect: UiSchemaRuleEffects.NONE, + }, + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [false], + }, + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [false], + }, + ], }, - ], - }, + }, + ], }, { - labelKey: `some.scope.fields.isSharedUpdate.label`, - scope: "/properties/isSharedUpdate", - type: "Control", - options: { - control: "hub-field-input-switch", - helperText: { - labelKey: `some.scope.fields.isSharedUpdate.helperText`, + type: "Section", + labelKey: `some.scope.sections.capabilities.label`, + elements: [ + { + labelKey: `some.scope.fields.isSharedUpdate.label`, + scope: "/properties/isSharedUpdate", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `some.scope.fields.isSharedUpdate.helperText`, + }, + }, + rule: { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [false], + }, + }, + { + labelKey: `some.scope.fields.isAdmin.label`, + scope: "/properties/leavingDisallowed", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `some.scope.fields.isAdmin.helperText`, + }, + }, + rules: [ + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [false], + }, + ], + }, + { + labelKey: `some.scope.fields.isOpenData.label`, + scope: "/properties/isOpenData", + type: "Control", + options: { + control: "hub-field-input-switch", + helperText: { + labelKey: `some.scope.fields.isOpenData.helperText`, + }, + messages: [ + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `some.scope.fields.isOpenData.constError`, + }, + ], + }, + rules: [ + { + effect: UiSchemaRuleEffects.ENABLE, + conditions: [ + { + scope: "/properties/access", + schema: { const: "public" }, + }, + false, + ], + }, + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/access", + schema: { not: { const: "public" } }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.SHOW, + conditions: [false], + }, + ], }, - }, + ], }, { - labelKey: `some.scope.fields.membershipAccess.label`, - scope: "/properties/membershipAccess", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - `{{some.scope.fields.membershipAccess.org:translate}}`, - `{{some.scope.fields.membershipAccess.collab:translate}}`, - `{{some.scope.fields.membershipAccess.any:translate}}`, - ], - rules: [ - [ + type: "Section", + labelKey: `some.scope.sections.membershipAccess.label`, + elements: [ + { + labelKey: `some.scope.fields.membershipAccess.label`, + scope: "/properties/membershipAccess", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{some.scope.fields.membershipAccess.org.label:translate}}`, + `{{some.scope.fields.membershipAccess.collab.label:translate}}`, + `{{some.scope.fields.membershipAccess.any.label:translate}}`, + ], + descriptions: [ + `{{some.scope.fields.membershipAccess.org.description:translate}}`, + `{{some.scope.fields.membershipAccess.collab.description:translate}}`, + `{{some.scope.fields.membershipAccess.any.description:translate}}`, + ], + // rules that pertain to the individual options + rules: [ + [ + { + effect: UiSchemaRuleEffects.NONE, + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + ], + }, + ], + ], + messages: [ + { + type: "ERROR", + keyword: "pattern", + icon: true, + labelKey: `some.scope.fields.membershipAccess.patternError`, + }, + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `some.scope.fields.membershipAccess.constError`, + }, + ], + }, + // rules that pertain to the control as a whole + rules: [ { - effect: UiSchemaRuleEffects.NONE, + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], }, - ], - [ { - effect: UiSchemaRuleEffects.DISABLE, - conditions: [true], + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + { + scope: "/properties/membershipAccess", + schema: { const: "anyone" }, + }, + ], }, ], - [ + }, + { + labelKey: `some.scope.fields.join.label`, + scope: "/properties/_join", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{some.scope.fields.join.invite.label:translate}}`, + `{{some.scope.fields.join.request.label:translate}}`, + `{{some.scope.fields.join.auto.label:translate}}`, + ], + descriptions: [ + `{{some.scope.fields.join.invite.description:translate}}`, + `{{some.scope.fields.join.request.description:translate}}`, + `{{some.scope.fields.join.auto.description:translate}}`, + ], + // rules that pertain to the individual options + rules: [ + [ + { + effect: UiSchemaRuleEffects.NONE, + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + ], + [ + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.DISABLE, + conditions: [ + { + scope: "/properties/isSharedUpdate", + schema: { const: true }, + }, + ], + }, + ], + ], + messages: [ + { + type: "ERROR", + keyword: "const", + icon: true, + labelKey: `some.scope.fields.join.constError`, + }, + { + type: "ERROR", + keyword: "pattern", + icon: true, + labelKey: `some.scope.fields.join.patternError`, + }, + ], + }, + // rules that pertain to the control as a whole + rules: [ { - effect: UiSchemaRuleEffects.DISABLE, - conditions: [true], + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/access", + schema: { const: "private" }, + }, + ], + }, + { + effect: UiSchemaRuleEffects.RESET, + conditions: [ + { + scope: "/properties/leavingDisallowed", + schema: { const: true }, + }, + { + scope: "/properties/_join", + schema: { const: "auto" }, + }, + ], }, { - effect: UiSchemaRuleEffects.DISABLE, + effect: UiSchemaRuleEffects.RESET, conditions: [ { scope: "/properties/isSharedUpdate", schema: { const: true }, }, + { + scope: "/properties/_join", + schema: { const: "auto" }, + }, ], }, ], - ], - messages: [ - { - type: "ERROR", - keyword: "enum", - icon: true, - labelKey: `some.scope.fields.membershipAccess.enumError`, + }, + { + labelKey: `some.scope.fields.hiddenMembers.label`, + scope: "/properties/hiddenMembers", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{some.scope.fields.hiddenMembers.members.label:translate}}`, + `{{some.scope.fields.hiddenMembers.admins.label:translate}}`, + ], + descriptions: [ + `{{some.scope.fields.hiddenMembers.members.description:translate}}`, + `{{some.scope.fields.hiddenMembers.admins.description:translate}}`, + ], }, - ], - }, - }, - { - labelKey: `some.scope.fields.contributeContent.label`, - scope: "/properties/isViewOnly", - type: "Control", - options: { - control: "hub-field-input-radio", - labels: [ - `{{some.scope.fields.contributeContent.all:translate}}`, - `{{some.scope.fields.contributeContent.admins:translate}}`, - ], - }, + }, + { + labelKey: `some.scope.fields.contributeContent.label`, + scope: "/properties/isViewOnly", + type: "Control", + options: { + control: "hub-field-input-tile-select", + labels: [ + `{{some.scope.fields.contributeContent.members.label:translate}}`, + `{{some.scope.fields.contributeContent.admins.label:translate}}`, + ], + descriptions: [ + `{{some.scope.fields.contributeContent.members.description:translate}}`, + `{{some.scope.fields.contributeContent.admins.description:translate}}`, + ], + }, + }, + ], }, ], }); @@ -126,9 +426,9 @@ describe("GroupUiSchemaCreate", () => { ); expect(defaults).toEqual({ - access: "org", + access: "private", autoJoin: false, - isSharedUpdate: false, + isSharedUpdate: true, isInvitationOnly: false, hiddenMembers: false, isViewOnly: false, @@ -144,9 +444,9 @@ describe("GroupUiSchemaCreate", () => { getMockContextWithPrivilenges(["portal:user:addExternalMembersToGroup"]) ); expect(defaults).toEqual({ - access: "org", + access: "private", autoJoin: false, - isSharedUpdate: false, + isSharedUpdate: true, isInvitationOnly: false, hiddenMembers: false, isViewOnly: false, diff --git a/packages/common/test/groups/_internal/convertHubGroupToGroup.test.ts b/packages/common/test/groups/_internal/convertHubGroupToGroup.test.ts index f0fc5aaf033..640a91c94a7 100644 --- a/packages/common/test/groups/_internal/convertHubGroupToGroup.test.ts +++ b/packages/common/test/groups/_internal/convertHubGroupToGroup.test.ts @@ -12,9 +12,10 @@ describe("groups: convertHubGroupToGroup:", () => { thumbnail: "group.jpg", membershipAccess: "collaborators", isSharedUpdate: true, + _join: "invite", } as unknown as IHubGroup; }); - it("converts an HubGroup to a IGroup", async () => { + it("converts an HubGroup with _join === invite to a IGroup", async () => { const chk = convertHubGroupToGroup(hubGroup); // we convert some props in HubGroup to something else // in IGroup, checking them is a good way to @@ -23,6 +24,32 @@ describe("groups: convertHubGroupToGroup:", () => { expect(chk.access).toBe("org"); expect(chk.membershipAccess).toBe("collaboration"); expect(chk.capabilities).toBe("updateitemcontrol"); + expect(chk.isInvitationOnly).toBe(true); + expect(chk.autoJoin).toBe(false); + }); + it("converts an HubGroup with _join === request to a IGroup", async () => { + const chk = convertHubGroupToGroup({ ...hubGroup, _join: "request" }); + // we convert some props in HubGroup to something else + // in IGroup, checking them is a good way to + // varify the HubGroup -> IGroup convertion + expect(chk.id).toBe("3ef"); + expect(chk.access).toBe("org"); + expect(chk.membershipAccess).toBe("collaboration"); + expect(chk.capabilities).toBe("updateitemcontrol"); + expect(chk.isInvitationOnly).toBe(false); + expect(chk.autoJoin).toBe(false); + }); + it("converts an HubGroup with _join === auto to a IGroup", async () => { + const chk = convertHubGroupToGroup({ ...hubGroup, _join: "auto" }); + // we convert some props in HubGroup to something else + // in IGroup, checking them is a good way to + // varify the HubGroup -> IGroup convertion + expect(chk.id).toBe("3ef"); + expect(chk.access).toBe("org"); + expect(chk.membershipAccess).toBe("collaboration"); + expect(chk.capabilities).toBe("updateitemcontrol"); + expect(chk.isInvitationOnly).toBe(false); + expect(chk.autoJoin).toBe(true); }); it("clears empty fields", async () => { hubGroup.membershipAccess = "anyone";