Skip to content

Commit

Permalink
feat: context has user history (#1411)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbouwman authored Feb 16, 2024
1 parent 522f7bc commit 1c350cd
Show file tree
Hide file tree
Showing 12 changed files with 814 additions and 8 deletions.
199 changes: 195 additions & 4 deletions packages/common/src/ArcGISContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,30 @@ import {
} from "@esri/arcgis-rest-auth";
import { IGetUserOptions, IPortal, getUser } from "@esri/arcgis-rest-portal";
import { IRequestOptions } from "@esri/arcgis-rest-request";
import { HubServiceStatus } from "./core";
import {
IHubHistory,
IHubHistoryEntry,
addHistoryEntry,
removeHistoryEntry,
} from "./core/hubHistory";
import { getProp, getWithDefault } from "./objects";
import { HubEnvironment, HubLicense, IFeatureFlags } from "./permissions/types";
import {
HubEnvironment,
HubLicense,
IFeatureFlags,
IPermissionAccessResponse,
Permission,
} from "./permissions/types";
import { IHubRequestOptions, IHubTrustedOrgsResponse } from "./types";
import { getEnvironmentFromPortalUrl } from "./utils/getEnvironmentFromPortalUrl";
import { IUserResourceToken, UserResourceApp } from "./ArcGISContextManager";
import { IUserHubSettings } from "./utils";
import {
IUserHubSettings,
updateUserHubSettings,
} from "./utils/hubUserAppResources";
import { HubServiceStatus } from "./core/types/ISystemStatus";
import { checkPermission } from "./permissions/checkPermission";
import { HubEntity } from "./core/types/HubEntity";

/**
* Hash of Hub API end points so updates
Expand Down Expand Up @@ -215,6 +232,12 @@ export interface IArcGISContext {
*/
userHubSettings?: IUserHubSettings;

/**
* Return the user's history
* @returns
*/
history: IHubHistory;

/**
* Return the token for a given app, if defined
* @param app
Expand All @@ -225,6 +248,40 @@ export interface IArcGISContext {
* Refresh the current user, including their groups
*/
refreshUser(): Promise<void>;

/**
* Add an entry to the user's history
* If not authenticated, this function will no-op
* @param entry
*/
addToHistory(entry: IHubHistoryEntry): Promise<void>;

/**
* Remove a specific entry from the user's history
* @param entry
*/
removeFromHistory(entry: IHubHistoryEntry): Promise<void>;

/**
* Simple clear of the all of the user's history
*/
clearHistory(): Promise<void>;

/**
* Check specific permission for the current user, and optionally an entity
* @param permission
* @param entity
*/
checkPermission(
permission: Permission,
entity?: HubEntity
): IPermissionAccessResponse;

/**
* Update the user's hub settings directly from the Context
* @param settings
*/
updateUserHubSettings(settings: IUserHubSettings): Promise<void>;
}

/**
Expand Down Expand Up @@ -386,7 +443,9 @@ export class ArcGISContext implements IArcGISContext {

this._featureFlags = opts.featureFlags || {};
this._userResourceTokens = opts.userResourceTokens || [];
this._userHubSettings = opts.userHubSettings || null;
this._userHubSettings = opts.userHubSettings || {
schemaVersion: 1,
};
}

/**
Expand Down Expand Up @@ -773,4 +832,136 @@ export class ArcGISContext implements IArcGISContext {
this._currentUser = user;
});
}

/**
* Return the user's history
* @returns
*/
public get history(): IHubHistory {
return this._userHubSettings.history || ({ entries: [] } as IHubHistory);
}

/**
* Check specific permission for the current user, and optionally an entity
* @param permission
* @param entity
* @returns
*/
checkPermission(
permission: Permission,
entity?: HubEntity
): IPermissionAccessResponse {
return checkPermission(permission, this, entity);
}

/**
* Add an entry to the user's history
* @param entry
* @param win
* @returns
*/
public async addToHistory(entry: IHubHistoryEntry): Promise<void> {
// No-op if not authenticated
if (!this.isAuthenticated) {
return;
}

// No-op if the user doesn't have the permission
const chk = this.checkPermission("hub:feature:history");
if (!chk.access) {
return;
}

// add the entry to the history
const updated = addHistoryEntry(entry, this.history);
// The getter reads from this, so just re-assign
this._userHubSettings.history = updated;
// update the user-app-resource
return this.updateUserHubSettings(this._userHubSettings);

// --------------------------------------------------------------------
// Turns out that integrating this with LocalStorage will involve
// a lot of syncronization as auth'd user moves between sites
// which may have different history states in localStorage
// So let's start with just tracking the history
// in workspaces and see how that goes.
// --------------------------------------------------------------------
}

/**
* Clear the entire history
* @returns
*/
public async clearHistory(): Promise<void> {
// No-op if not authenticated
if (!this.isAuthenticated) {
return;
}
// No-op if the user doesn't have the permission
const chk = this.checkPermission("hub:feature:history");
if (!chk.access) {
return;
}
// create a new history object
const history: IHubHistory = {
entries: [],
};
// assign into the user-app-resource
this._userHubSettings.history = history;
// update the user-app-resource
return this.updateUserHubSettings(this._userHubSettings);
}

/**
* Remove a specific entry from the user's history
* @param entry
* @returns
*/
public async removeFromHistory(entry: IHubHistoryEntry): Promise<void> {
// No-op if not authenticated
if (!this.isAuthenticated) {
return;
}
// No-op if the user doesn't have the permission
const chk = this.checkPermission("hub:feature:history");
if (!chk.access) {
return;
}
// remove the entry from the history
const updated = removeHistoryEntry(entry, this.history);
this._userHubSettings.history = updated;
// update the user-app-resource
return this.updateUserHubSettings(this._userHubSettings);
}

/**
* Update the user's hub settings
*
* Possible issue here is that we're not updating the contextManager
* so the contextManager will be out of sync with this instance of
* ArcGISContext. Unclear if this will be an actual problem.
* @param settings
*/
public async updateUserHubSettings(
settings: IUserHubSettings
): Promise<void> {
if (!this._authentication) {
throw new Error(
"Cannot update user hub settings without an authenticated user"
);
}
// update the user-app-resource
await updateUserHubSettings(settings, this);
// update the context
this._userHubSettings = settings;
// update the feature flags
Object.keys(getWithDefault(settings, "preview", {})).forEach((key) => {
// only set the flag if it's true, otherwise delete the flag so we revert to default behavior
if (getProp(settings, `preview.${key}`)) {
this._featureFlags[`hub:feature:${key}`] = true;
} else {
delete this._featureFlags[`hub:feature:${key}`];
}
});
}
}
121 changes: 121 additions & 0 deletions packages/common/src/core/hubHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { cloneObject } from "../util";
import { HubEntityType } from "./types/HubEntityType";

/**
* History is a way to track a set of sites/projects/initiatives/pages that a user has visited
* Any content can be added to the history, but there will be different limits for different types
* Please see the addHistoryEntry function for more details
*/
export interface IHubHistory {
// Modelled as an object so we can easily extend without requiring a schema change
entries: IHubHistoryEntry[];
}

/**
* History entry
*/
export interface IHubHistoryEntry {
/**
* Entity Type
*/
type: HubEntityType;
/**
* What action was last taken on the entity
* In the future this can expand to "replied to", "shared with" etc
*/
action: "view" | "workspace";
/**
* Url the user visited
*/
url: string;
/**
* Unique identifier for the entity
*/
id: string;
/**
* Title of the entity
*/
title: string;
/**
* Owner of the entity
*/
owner?: string;
/**
* timestamp the entity was last visited
*/
visited?: number;
/**
* Additional parameters that can be used to rehydrate the entity
*/
params?: any;
}

/**
* Add an entry to the history. This handles the limits per type,
* and ensures that the most recent entry is at the top of the list.
* This function does not persist the history, it only updates the object.
* @param entry
* @param history
* @returns
*/
export function addHistoryEntry(
entry: IHubHistoryEntry,
history: IHubHistory
): IHubHistory {
// Define limits to how many of each type of history we store
const limits = {
sites: 10,
entities: 50,
};

// Anything other than "site" is considered an "entity" for history purposes
// When we disply the list of history, we use the underlying type to determine the icon and route
const type = entry.type === "site" ? "sites" : "entities";
// get a clone of the existing entries
let entries: IHubHistoryEntry[] = cloneObject(history.entries);
// split into two arrays, one for the type and one for everything else
const siteEntries = entries.filter((e) => e.type === "site");
const otherEntries = entries.filter((e) => e.type !== "site");

// Depending on the type, choose the list to work with
if (entry.type === "site") {
entries = siteEntries;
} else {
entries = otherEntries;
}
// If there is an existing entry, remove it
entries = entries.filter((e) => e.id !== entry.id);
// if the count of the type is below the limit, add the new entry to the start of the array
if (entries.length < limits[type]) {
entries.unshift(entry);
} else {
// otherwise, remove the last entry and add the new entry to the start of the array
entries.pop();
entries.unshift(entry);
}
// merge up the arrays and sort by visited date
// depending on tne type, we may need to merge the arrays in a different order
if (entry.type === "site") {
history.entries = [...entries, ...otherEntries];
} else {
history.entries = [...siteEntries, ...entries];
}

// return the updated history
return history;
}

/**
* Remove a specific entry from the user's history
* @param entry
* @returns
*/
export function removeHistoryEntry(
entry: IHubHistoryEntry,
history: IHubHistory
): IHubHistory {
// remove the entry from the history
const updated = cloneObject(history);
updated.entries = updated.entries.filter((e) => e.id !== entry.id);
return updated;
}
5 changes: 3 additions & 2 deletions packages/common/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./getRelativeWorkspaceUrl";
export * from "./isValidEntityType";
export * from "./processActionLinks";

// For sme reason, if updateHubEntity is exported here,
// it is not actually exported in the final package.
// For sme reason, if these are exported here,
// they are not actually exported in the final package.
// export * from "./updateHubEntity";
// export * from "./hubHistory";
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export * from "./core/EntityEditor";
// Unclear _why_ this needs to be here vs. in search/index.ts
// but if it's exported there, it's not actually exporeted
export * from "./search/explainQueryResult";
export * from "./core/hubHistory";

import OperationStack from "./OperationStack";
import OperationError from "./OperationError";
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/permissions/HubPermissionPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ const SystemPermissionPolicies: IPermissionPolicy[] = [
{
// when enabled keyboard shortcuts will be available
permission: "hub:feature:keyboardshortcuts",
availability: ["alpha"],
environments: ["devext", "qaext"],
},
{
// Enables the history feature
permission: "hub:feature:history",
availability: ["alpha"],
environments: ["devext", "qaext"],
},
];
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/permissions/checkPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function checkPermission(

// We also check the context.userHubSettings.preview array
// which can also be used to enable features.
if (context.userHubSettings?.preview) {
if (context.userHubSettings.preview) {
const preview = getWithDefault(
context,
"userHubSettings.preview",
Expand Down
Loading

0 comments on commit 1c350cd

Please sign in to comment.