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: user workspace pane permissions & add content config #1667

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/common/src/content/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
deriveLocationFromItem,
getHubRelativeUrl,
} from "./_internal/internalContentUtils";
import { getRelativeWorkspaceUrl } from "../core/getRelativeWorkspaceUrl";

/**
* Enrich a generic search result
Expand Down Expand Up @@ -89,6 +90,10 @@ export async function enrichContentSearchResult(
result.id,
item.typeKeywords
);
result.links.workspaceRelative = getRelativeWorkspaceUrl(
result.type,
result.id
);

return result;
}
44 changes: 44 additions & 0 deletions packages/common/src/search/_internal/getUserGroupsByMembership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IUser } from "@esri/arcgis-rest-types";
import { IGroupsByMembership } from "../types/IGroupsByMembership";

/**
* Retrieves the user's groups categorized by their membership type.
*
* @param user - The user object containing group information.
* @returns An object categorizing the user's groups into `owner`, `admin`, and `member`.
*
* The function processes the user's groups and classifies them based on the membership type:
* - `owner`: Groups where the user is an owner.
* - `admin`: Groups where the user is an admin.
* - `member`: Groups where the user is a member and the group is not view-only.
*
* Note: The `none` membership type is not considered as it is not expected to be present in the user's groups.
*/

export function getUserGroupsByMembership(user: IUser): IGroupsByMembership {
const response: IGroupsByMembership = {
owner: [],
member: [],
admin: [],
};
// get the user's groups
const userGroups = user.groups || [];
// loop through the groups and determine if the user is an admin or normal member
// and add into the response
userGroups.forEach((group) => {
if (group.userMembership?.memberType === "owner") {
response.owner.push(group.id);
}
if (group.userMembership?.memberType === "admin") {
response.admin.push(group.id);
}
// If user is just a member and the group is not view only
if (group.userMembership?.memberType === "member" && !group.isViewOnly) {
response.member.push(group.id);
}
// there is a `none` option in the userMembership but
// that would never be returned in the user's groups
// so we don't need to check for it
});
return response;
}
47 changes: 23 additions & 24 deletions packages/common/src/search/_internal/getUserGroupsFromQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IUser } from "@esri/arcgis-rest-types";
import { getPredicateValues } from "../getPredicateValues";
import { IGroupsByMembership } from "../types/IGroupsByMembership";
import { IQuery } from "../types/IHubCatalog";
import { getUserGroupsByMembership } from "./getUserGroupsByMembership";
/**
* Given a query and a user, return an object with the set of groups
* that are in the Query, and which the user is a member of, split by
Expand All @@ -16,37 +17,35 @@ export function getUserGroupsFromQuery(
query: IQuery,
user: IUser
): IGroupsByMembership {
const response: IGroupsByMembership = {
let response: IGroupsByMembership = {
owner: [],
member: [],
admin: [],
};
// collect up all the group predicates from the query's filters
// NOTE: this only pulls the all and any predicates
const groups: string[] = getPredicateValues("group", query);
// get the user's groups by membership
const allUserGroups = getUserGroupsByMembership(user);
// if there are groups in the query, we subset the user's groups
// based on the groups in the query
if (groups.length) {
const props: Array<keyof IGroupsByMembership> = [
"owner",
"admin",
"member",
];
groups.forEach((groupId) => {
// check each group type and add the group to the response if the user is a member
props.forEach((prop) => {
if (allUserGroups[prop].includes(groupId)) {
response[prop].push(groupId);
}
});
});
} else {
response = allUserGroups;
}

// get the user's groups
const userGroups = user.groups || [];
// loop through the groups and determine if the user is an admin or normal member
// and add into the response
groups.forEach((groupId) => {
// get the group from the user's groups array
const group = userGroups.find((g) => g.id === groupId);
if (group) {
if (group.userMembership?.memberType === "owner") {
response.owner.push(groupId);
}
if (group.userMembership?.memberType === "admin") {
response.admin.push(groupId);
}
// If user is just a member and the group is not view only
if (group.userMembership?.memberType === "member" && !group.isViewOnly) {
response.member.push(groupId);
}
// there is a `none` option in the userMembership but
// that would never be returned in the user's groups
// so we don't need to check for it
}
});
return response;
}
1 change: 1 addition & 0 deletions packages/common/src/search/_internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./hubSearchEvents";
export * from "./hubSearchEventAttendees";
export * from "./getWorkflowForType";
export * from "./getCatalogGroups";
export * from "./getUserGroupsByMembership";
67 changes: 57 additions & 10 deletions packages/common/src/search/getAddContentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
} from "./_internal/getWorkflowForType";
import { getProp } from "../objects/get-prop";
import { getUserGroupsFromQuery } from "./_internal/getUserGroupsFromQuery";
import { getUserGroupsByMembership } from "./_internal/getUserGroupsByMembership";
import { IAddContentWorkflowConfig } from "./types/AddContentWorkflowTypes";
import { getCatalogGroups } from "./_internal";
import { IGroupsByMembership } from "./types/IGroupsByMembership";

const EmptyAddContentWorkflowConfig: IAddContentWorkflowConfig = {
create: null,
Expand Down Expand Up @@ -144,6 +146,8 @@ function getAddContentConfigForQuery(
return getAddContentConfigForItemQuery(query, context);
} else if (query.targetEntity === "event") {
return getAddContentConfigForEventQuery(query, context);
} else if (query.targetEntity === "group") {
return getAddContentConfigForGroupQuery(query, context);
} else {
const response = cloneObject(EmptyAddContentWorkflowConfig);
response.state = "disabled";
Expand All @@ -152,6 +156,31 @@ function getAddContentConfigForQuery(
}
}

function getAddContentConfigForGroupQuery(
_query: IQuery,
context: IArcGISContext
): IAddContentWorkflowConfig {
const response = cloneObject(EmptyAddContentWorkflowConfig);
// groups can be created or added but the user needs permission
const chk = checkPermission("hub:group:create", context);
if (chk.access) {
response.create = {
targetEntity: "group",
workflow: "create",
types: ["Group"],
};
response.state = "enabled";
} else {
response.state = "disabled";
response.reason = "no-permission";
if (chk.response === "assertion-failed") {
response.reason = "too-many-groups";
}
}

return response;
}

/**
* Specific logic for targetEntity="event"
* @param query
Expand Down Expand Up @@ -210,16 +239,31 @@ function getAddContentConfigForItemQuery(
context: IArcGISContext
): IAddContentWorkflowConfig {
const response = cloneObject(EmptyAddContentWorkflowConfig);
const userGroups = getUserGroupsFromQuery(query, context.currentUser);
if (
!userGroups.owner.length &&
!userGroups.admin.length &&
!userGroups.member.length
) {
response.state = "disabled";
response.reason = "not-in-groups";
return response;
// We need to return groups by membership so we can show the group sharing ux
let userGroups: IGroupsByMembership = {
owner: [],
admin: [],
member: [],
};

// If the query has groups, then we need to check that the user is a member of those groups
const groups: string[] = getPredicateValues("group", query);
if (groups.length) {
userGroups = getUserGroupsFromQuery(query, context.currentUser);
if (
!userGroups.owner.length &&
!userGroups.admin.length &&
!userGroups.member.length
) {
response.state = "disabled";
response.reason = "not-in-groups";
return response;
}
} else {
// we just need all the user's groups, as IGroupsByMembership object
userGroups = getUserGroupsByMembership(context.currentUser);
}

// Get all the types from all the the predicates in all of the filters
let queryTypes = getPredicateValues("type", query);

Expand Down Expand Up @@ -248,7 +292,10 @@ function getAddContentConfigForItemQuery(
}
response.create.types.push(wft.type);
}
if (wft.workflows.includes("existing")) {
// Only show the add existing workflows if the query includes groups
// otherwise the query is defined by other criteria that we likely can't
// handle at this time (e.g. items owned by current user)
if (wft.workflows.includes("existing") && groups.length) {
if (!response.existing) {
response.existing = {
targetEntity: wft.targetEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface IAddContentWorkflowConfig {
| "no-permission"
| "not-in-groups"
| "invalid-object"
| "too-many-groups"
| "unsupported-target-entity";
// FUTURE we can add the checks
}
Expand Down
25 changes: 20 additions & 5 deletions packages/common/src/users/_internal/UserBusinessRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const UserPermissions = [
"hub:user:workspace",
"hub:user:workspace:overview",
"hub:user:workspace:settings",
"hub:user:workspace:content",
"hub:user:workspace:groups",
"hub:user:workspace:shared-with-me",
"hub:user:manage",
] as const;

Expand Down Expand Up @@ -51,14 +54,26 @@ export const UserPermissionPolicies: IPermissionPolicy[] = [
},
{
permission: "hub:user:workspace:overview",
// NOTE: other entities like site, group, and content gate this to alpha
// but at least for now this is the only pane for users, so no gating
// availability: ["alpha"],
dependencies: ["hub:user:workspace", "hub:user:view"],
dependencies: ["hub:user:workspace"],
},
{
permission: "hub:user:workspace:content",
availability: ["alpha"],
dependencies: ["hub:user:workspace", "hub:user:owner"],
},
{
permission: "hub:user:workspace:groups",
availability: ["alpha"],
dependencies: ["hub:user:workspace", "hub:user:owner"],
},
{
permission: "hub:user:workspace:shared-with-me",
availability: ["alpha"],
dependencies: ["hub:user:workspace", "hub:user:owner"],
},
{
permission: "hub:user:workspace:settings",
dependencies: ["hub:user:workspace", "hub:user:edit"],
dependencies: ["hub:user:workspace", "hub:user:owner"],
},
{
permission: "hub:user:manage",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { IQuery } from "../../../src";
import { IUser } from "../../../src/events/api";
import { getUserGroupsFromQuery } from "../../../src/search/_internal/getUserGroupsFromQuery";
import * as GetUserGroupsByMembershipModule from "../../../src/search/_internal/getUserGroupsByMembership";

describe("getUserGroupsFromQuery:", () => {
it("returns all user groups by membership if query lacks group predicate", async () => {
const query: IQuery = {
targetEntity: "item",
filters: [],
};
const user: IUser = {
username: "user1",
} as unknown as IUser;

const getUserGroupsByMembershipSpy = spyOn(
GetUserGroupsByMembershipModule,
"getUserGroupsByMembership"
).and.callFake(() => {
return {
owner: ["o00"],
member: ["m00"],
admin: ["a00"],
};
});

const result = await getUserGroupsFromQuery(query, user);
expect(result).toEqual({
owner: ["o00"],
member: ["m00"],
admin: ["a00"],
});
expect(getUserGroupsByMembershipSpy).toHaveBeenCalled();
});

it("returns only groups user is a member of, from groups in query", async () => {
const query: IQuery = {
targetEntity: "item",
filters: [
{
predicates: [
{
group: "m00",
},
],
},
],
};
const user: IUser = {
username: "user1",
} as unknown as IUser;

const getUserGroupsByMembershipSpy = spyOn(
GetUserGroupsByMembershipModule,
"getUserGroupsByMembership"
).and.callFake(() => {
return {
owner: ["o00"],
member: ["m00"],
admin: ["a00"],
};
});
const result = await getUserGroupsFromQuery(query, user);
expect(result).toEqual({
owner: [],
member: ["m00"],
admin: [],
});
expect(getUserGroupsByMembershipSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ describe("hubSearchItems Module |", () => {
links: {
self: "https://www.arcgis.com/home/item.html?id=f4bcc",
siteRelative: "/maps/f4bcc",
workspaceRelative: "/workspace/content/f4bcc",
thumbnail:
"https://www.arcgis.com/sharing/rest/content/items/f4bcc/info/thumbnail/hub_thumbnail_1658341016537.png",
},
Expand Down
Loading
Loading