Skip to content

Commit

Permalink
feat: followers settings schema and behavior (#1211)
Browse files Browse the repository at this point in the history
- adds IWithFollowersBehavior which exposes getFollowers and setFollowersAccess on all item-backed entities
- adds a new SiteUiSchemaFollowers and adds the toEditor/fromEditor logic
- adds convertFeaturesToLegacyCapabilities and migrateLegacyCapabilitiesToFeatures functions that get executed when we call updateSite and fetchSite respectively
  • Loading branch information
juliannemarik authored Sep 18, 2023
1 parent e818a0a commit a8be73b
Show file tree
Hide file tree
Showing 27 changed files with 712 additions and 26 deletions.
32 changes: 30 additions & 2 deletions packages/common/src/core/HubItemEntity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
IGroup,
IItem,
getGroup,
removeItemResource,
setItemAccess,
shareItemWithGroup,
unshareItemWithGroup,
updateGroup,
} from "@esri/arcgis-rest-portal";
import { IArcGISContext } from "../ArcGISContext";
import HubError from "../HubError";
Expand All @@ -29,10 +31,11 @@ import {
} from "./behaviors";

import { IWithThumbnailBehavior } from "./behaviors/IWithThumbnailBehavior";
import { IHubItemEntity, SettableAccessLevel } from "./types";
import { AccessLevel, IHubItemEntity, SettableAccessLevel } from "./types";
import { sharedWith } from "./_internal/sharedWith";
import { IWithDiscussionsBehavior } from "./behaviors/IWithDiscussionsBehavior";
import { setDiscussableKeyword } from "../discussions";
import { IWithFollowersBehavior } from "./behaviors/IWithFollowersBehavior";

const FEATURED_IMAGE_FILENAME = "featuredImage.png";

Expand All @@ -46,7 +49,8 @@ export abstract class HubItemEntity<T extends IHubItemEntity>
IWithThumbnailBehavior,
IWithFeaturedImageBehavior,
IWithPermissionBehavior,
IWithDiscussionsBehavior
IWithDiscussionsBehavior,
IWithFollowersBehavior
{
protected context: IArcGISContext;
protected entity: T;
Expand Down Expand Up @@ -222,6 +226,30 @@ export abstract class HubItemEntity<T extends IHubItemEntity>
this.entity.access = access;
}

/**
* Returns the followers group
*/
async getFollowersGroup(): Promise<IGroup> {
return getGroup(
this.entity.followersGroupId,
this.context.userRequestOptions
);
}

/**
* Sets the access level of the followers group
* @param access
*/
async setFollowersGroupAccess(access: SettableAccessLevel): Promise<void> {
await updateGroup({
group: {
id: this.entity.followersGroupId,
access,
},
authentication: this.context.session,
});
}

/**
* Return a list of groups the Entity is shared to.
* @returns
Expand Down
16 changes: 16 additions & 0 deletions packages/common/src/core/behaviors/IWithFollowersBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IGroup } from "@esri/arcgis-rest-types";
import { SettableAccessLevel } from "../types";

/**
* Followers behavior for Item-Backed Entities
*/
export interface IWithFollowersBehavior {
/**
* Get the followers group
*/
getFollowersGroup(): Promise<IGroup>;
/**
* Set the access level of the followers group
*/
setFollowersGroupAccess(access: SettableAccessLevel): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export async function getEntityEditorSchemas(
import("../../../sites/_internal/SiteUiSchemaEdit"),
"hub:site:create": () =>
import("../../../sites/_internal/SiteUiSchemaCreate"),
"hub:site:followers": () =>
import("../../../sites/_internal/SiteUiSchemaFollowers"),
}[type as SiteEditorType]();
uiSchema = await siteModule.buildUiSchema(
i18nScope,
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/core/schemas/internal/subsetSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export function subsetSchema(
props: string[]
): IConfigurationSchema {
const subset: IConfigurationSchema = cloneObject(schema);

// 1. remove un-specified properties from the "required" array
subset.required = [...subset.required].filter((required) =>
props.includes(required)
);

// 2. filter the rest of the schema down to the specified properties
Object.keys(subset.properties).forEach((key) => {
if (props.indexOf(key) === -1) {
delete subset.properties[key];
Expand Down
13 changes: 13 additions & 0 deletions packages/common/src/core/schemas/shared/HubItemEntitySchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ export const HubItemEntitySchema: IConfigurationSchema = {
categories: ENTITY_CATEGORIES_SCHEMA,
isDiscussable: ENTITY_IS_DISCUSSABLE_SCHEMA,
_thumbnail: ENTITY_IMAGE_SCHEMA,
_followers: {
type: "object",
properties: {
groupAccess: {
...ENTITY_ACCESS_SCHEMA,
enum: ["private", "org", "public"],
},
showFollowAction: {
type: "boolean",
default: true,
},
},
},
view: {
type: "object",
properties: {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/core/traits/IWithFollowers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
*/
export interface IWithFollowers {
/** followers group id */
followersGroupId: string;
followersGroupId?: string;
}
21 changes: 20 additions & 1 deletion packages/common/src/core/types/IHubItemEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import {
IWithDiscussions,
} from "../traits";
import { IHubLocation } from "./IHubLocation";
import { IWithFollowers } from "../traits/IWithFollowers";

/**
* Properties exposed by Entities that are backed by Items
*/
export interface IHubItemEntity
extends IHubEntityBase,
IWithPermissions,
IWithDiscussions {
IWithDiscussions,
IWithFollowers {
/**
* Access level of the item ("private" | "org" | "public")
*/
Expand Down Expand Up @@ -128,3 +130,20 @@ export interface IHubItemEntity
*/
protected?: boolean;
}

export type IHubItemEntityEditor<T> = Omit<T, "extent"> & {
/**
* Thumbnail image. This is only used on the Editor and is
* persisted in the fromEditor method on the Class
*/
_thumbnail?: any;
/**
* Follower group settings. These settings are only used in the
* Editor and is persisted appropriately in the fromEditor
* method on the Class
*/
_followers?: {
groupAccess?: AccessLevel;
showFollowAction?: boolean;
};
};
14 changes: 3 additions & 11 deletions packages/common/src/core/types/IHubSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import {
IWithPermissions,
IWithSlug,
} from "../traits/index";
import { IHubItemEntity } from "./IHubItemEntity";
import { IWithFollowers } from "../traits/IWithFollowers";
import { IHubItemEntity, IHubItemEntityEditor } from "./IHubItemEntity";

/**
* DRAFT: Under development and more properties will likely be added
Expand All @@ -19,8 +18,7 @@ export interface IHubSite
IWithCatalog,
IWithLayout,
IWithPermissions,
IWithVersioningBehavior,
IWithFollowers {
IWithVersioningBehavior {
/**
* Array of minimal page objects
*/
Expand Down Expand Up @@ -83,10 +81,4 @@ export interface IHubSite
legacyTeams: string[];
}

export type IHubSiteEditor = Omit<IHubSite, "extent"> & {
/**
* Thumbnail image. This is only used on the Editor and is
* persisted in the fromEditor method on the Class
*/
_thumbnail?: any;
};
export type IHubSiteEditor = IHubItemEntityEditor<IHubSite> & {};
33 changes: 26 additions & 7 deletions packages/common/src/sites/HubSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { cloneObject } from "../util";
import { PropertyMapper } from "../core/_internal/PropertyMapper";
import { getPropertyMap } from "./_internal/getPropertyMap";

import { IHubSiteEditor, IModel } from "../index";
import { IHubSiteEditor, IModel, SettableAccessLevel } from "../index";
import { SiteEditorType } from "./_internal/SiteSchema";

/**
Expand Down Expand Up @@ -403,10 +403,15 @@ export class HubSite
* @returns
*/
toEditor(editorContext: IEntityEditorContext = {}): IHubSiteEditor {
// Cast the entity to it's editor
// 1. Cast entity to editor
const editor = cloneObject(this.entity) as IHubSiteEditor;

// Add other transforms here...
// 2. Apply transforms to relevant entity values so they
// can be consumed by the editor
editor._followers = {};
editor._followers.showFollowAction =
this.entity.features["hub:site:feature:follow"];

return editor;
}

Expand All @@ -418,6 +423,9 @@ export class HubSite
async fromEditor(editor: IHubSiteEditor): Promise<IHubSite> {
const isCreate = !editor.id;

// 1. Perform any pre-save operations e.g. storing
// image resources on the item, setting access, etc.

// Setting the thumbnailCache will ensure that
// the thumbnail is updated on next save
if (editor._thumbnail) {
Expand All @@ -436,18 +444,29 @@ export class HubSite

delete editor._thumbnail;

// convert back to an entity. Apply any reverse transforms used in
// of the toEditor method
// set the followers group access
if (editor._followers?.groupAccess) {
await this.setFollowersGroupAccess(
editor._followers.groupAccess as SettableAccessLevel
);
}

// 2. Convert editor values back to an entity e.g. apply
// any reverse transforms used in the toEditor method
const entity = cloneObject(editor) as IHubSite;

entity.features = {
...entity.features,
"hub:site:feature:follow": editor._followers?.showFollowAction,
};

// copy the location extent up one level
entity.extent = editor.location?.extent;

// create it if it does not yet exist...
// 3. create or update the in-memory entity and save
if (isCreate) {
throw new Error("Cannot create content using the Editor.");
} else {
// ...otherwise, update the in-memory entity and save it
this.entity = entity;
await this.save();
}
Expand Down
15 changes: 15 additions & 0 deletions packages/common/src/sites/HubSites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { setDiscussableKeyword } from "../discussions";
import { applyDefaultCollectionMigration } from "./_internal/applyDefaultCollectionMigration";
import { reflectCollectionsToSearchCategories } from "./_internal/reflectCollectionsToSearchCategories";
import { convertCatalogToLegacyFormat } from "./_internal/convertCatalogToLegacyFormat";
import { convertFeaturesToLegacyCapabilities } from "./_internal/capabilities/convertFeaturesToLegacyCapabilities";
export const HUB_SITE_ITEM_TYPE = "Hub Site Application";
export const ENTERPRISE_SITE_ITEM_TYPE = "Site Application";

Expand Down Expand Up @@ -345,6 +346,20 @@ export async function updateSite(
// with the existing structure on the most current model.
// TODO: Remove once the application is plumbed to work off an IHubCatalog
modelToUpdate = convertCatalogToLegacyFormat(modelToUpdate, currentModel);
/**
* Site capabilities are currently saved as an array on the
* site.data.values.capabilities. We want to migragte these
* legacy capabilities over to features in the new permissions
* system; however, we must continue persisting updates to
* these features in the legacy capabilities array until the
* existing site capabilities in our application are plumbed
* to work off of permissions
* TODO: Remove once site capabilities use permissions
*/
modelToUpdate = convertFeaturesToLegacyCapabilities(
modelToUpdate,
currentModel
);

// send updates to the Portal API and get back the updated site model
const updatedSiteModel = await updateModel(
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/sites/_internal/SiteBusinessRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SiteDefaultFeatures: IFeatureFlags = {
"hub:site:events": false,
"hub:site:content": true,
"hub:site:discussions": false,
"hub:site:feature:follow": true,
};

/**
Expand All @@ -23,6 +24,7 @@ export const SitePermissions = [
"hub:site:events",
"hub:site:content",
"hub:site:discussions",
"hub:site:feature:follow",
"hub:site:workspace:overview",
"hub:site:workspace:dashboard",
"hub:site:workspace:details",
Expand Down Expand Up @@ -81,6 +83,11 @@ export const SitesPermissionPolicies: IPermissionPolicy[] = [
permission: "hub:site:discussions",
dependencies: ["hub:site:view"],
},
{
permission: "hub:site:feature:follow",
dependencies: ["hub:site:view"],
entityConfigurable: true,
},
{
permission: "hub:site:workspace:overview",
dependencies: ["hub:site:view"],
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/sites/_internal/SiteSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { IConfigurationSchema } from "../../core";
import { HubItemEntitySchema } from "../../core/schemas/shared/HubItemEntitySchema";

export type SiteEditorType = (typeof SiteEditorTypes)[number];
export const SiteEditorTypes = ["hub:site:edit", "hub:site:create"] as const;
export const SiteEditorTypes = [
"hub:site:edit",
"hub:site:create",
"hub:site:followers",
] as const;

/**
* defines the JSON schema for a Hub Site's editable fields
Expand Down
6 changes: 3 additions & 3 deletions packages/common/src/sites/_internal/SiteUiSchemaEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { IHubSite } from "../../core/types";

/**
* @private
* construct edit uiSchema for Hub Projects - this defines
* how the schema properties should be rendered in the
* project editing experience
* constructs the edit uiSchema for Hub Sites.
* This defines how the schema properties should
* be rendered in the site editing experience
*/
export const buildUiSchema = async (
i18nScope: string,
Expand Down
Loading

0 comments on commit a8be73b

Please sign in to comment.