Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: followers settings schema and behavior #1211

Merged
merged 26 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a7ae4c1
feat: add IWithFollowersBehavior and add to HubItemEntity
juliannemarik Sep 12, 2023
4c18854
feat: extend IHubItemEntity with IWithFollowers trait
juliannemarik Sep 12, 2023
af21fb9
feat: add followers properties to base item entity schema
juliannemarik Sep 12, 2023
dc8ddc5
feat: add new site followers uiSchema
juliannemarik Sep 12, 2023
8a9dd0a
chore: update IWithEditorBehavior toEditor method to be async
juliannemarik Sep 12, 2023
a26576c
feat: add toEditor and fromEditor logic for site followers
juliannemarik Sep 12, 2023
0271d3f
fix: refactor to use permissions and add legacy capability migration
juliannemarik Sep 12, 2023
eda68a9
fix: make followersGroupId optional
juliannemarik Sep 13, 2023
dd2cc98
chore: add test for convertFeaturesToLegacyCapabilities
juliannemarik Sep 13, 2023
0cfb057
fix: ensure required properties are subset when we filter schema to u…
juliannemarik Sep 13, 2023
81b11df
chore: update IWithFollowersBehavior
juliannemarik Sep 13, 2023
2d99b09
chore: revert async toEditor behavior for now
juliannemarik Sep 13, 2023
d06ce53
chore: cleanup
juliannemarik Sep 13, 2023
0ebf8eb
fix: update getFollowers function in IWithFollowersBehavior
juliannemarik Sep 13, 2023
c0d6e4d
chore: update tests
juliannemarik Sep 13, 2023
ef37951
chore: add function comment to convertFeaturesToLegacyCapabilities
juliannemarik Sep 13, 2023
c707133
fix: rename IWithFollowersBehavior methods
juliannemarik Sep 13, 2023
8d8fe5e
chore: rename feature flag
juliannemarik Sep 14, 2023
f632210
feat: add capabilities module that takes card of forward/backward leg…
juliannemarik Sep 14, 2023
9b8ff71
chore: add/update tests
juliannemarik Sep 14, 2023
bebc3e8
chore: comment cleanup
juliannemarik Sep 14, 2023
dc0f2e4
fix: change access to groupAccess on schema
juliannemarik Sep 14, 2023
4f61169
fix: failing uiSchema test
juliannemarik Sep 14, 2023
18ab559
fix: minor cleanup
juliannemarik Sep 14, 2023
3b85be3
chore: add additional comments
juliannemarik Sep 15, 2023
6d251c5
fix: move migrateLegacyCapabilitiesToFeatures to upgrade-site-schema
juliannemarik Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does _followers need to be on the base schema, or should it only exist on SiteSchema?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good callout - I put it here because although the concept of a followers group only exists for sites today, I think the idea is that any item-backed entity could have a followers group in the future. That's also why I've added the IWithFollowers and IWithFollowersBehavior to the IHubItemEntity and HubItemEntity respectively rather than just to the HubSite.

With that said, if you think it would make more sense to keep this scoped to sites until followers are actually implemented for other entities, I can move it for now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks makes sense to leave it for potential future use cases. Looks good!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will add followers for Initiative and Project - the only reason it's on now Site is b/c that was/is synonymous with "Initiative".

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 @@ -80,10 +78,4 @@ export interface IHubSite
legacyCapabilities: 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw- these incremental comments are great - I'm going to start doing this

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