Skip to content

feat: add invite users to hub group #1197

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

Merged
merged 4 commits into from
Aug 31, 2023
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/common/src/groups/HubGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetchGroupEnrichments } from "./_internal/enrichments";
import { getProp, setProp } from "../objects";
import { getGroupThumbnailUrl, IHubSearchResult } from "../search";
import { parseInclude } from "../search/_internal/parseInclude";
import { IHubRequestOptions, IModel } from "../types";
import { IHubRequestOptions } from "../types";
import { getGroupHomeUrl } from "../urls";
import { unique } from "../util";
import { mapBy } from "../utils";
Expand Down
231 changes: 231 additions & 0 deletions packages/common/src/groups/_internal/AddOrInviteUsersToGroupUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import {
IAddOrInviteContext,
IAddOrInviteResponse,
IUserOrgRelationship,
IUserWithOrgType,
} from "../types";

import { processAutoAddUsers } from "./processAutoAddUsers";
import { processInviteUsers } from "./processInviteUsers";

// Add or invite flow based on the type of user begins here

/**
* @private
* Handles add/invite logic for collaboration coordinators inside partnered orgs.
* This is intentionally split out from the invitation of partnered org normal members,
* because the two types of partnered org usres (regular and collaboration coordinator)
* always come from the same 'bucket', however have distinctly different add paths Invite vs auto add.
* It returns either an empty instance of the addOrInviteResponse
* object, or their response from auto adding users.
*
* @export
* @param {IAddOrInviteContext} context context object
* @return {IAddOrInviteResponse} response object
*/
export async function addOrInviteCollaborationCoordinators(
context: IAddOrInviteContext
): Promise<IAddOrInviteResponse> {
// If there are no org users return handling no users
if (
!context.collaborationCoordinator ||
context.collaborationCoordinator.length === 0
) {
// we return an empty object because
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
return handleNoUsers();
}
return processAutoAddUsers(context, "collaborationCoordinator");
}

/**
* @private
* Handles add/invite logic for community users
* It returns either an empty instance of the addOrInviteResponse
* object, or either ther esponse from processing auto adding
* users or inviting users. If an email has been passed in it also notifies
* processAutoAddUsers that emails should be sent.
*
* @export
* @param {IAddOrInviteContext} context context object
* @return {IAddOrInviteResponse} response object
*/
export async function addOrInviteCommunityUsers(
context: IAddOrInviteContext
): Promise<IAddOrInviteResponse> {
// We default to handleNoUsers
// we return an empty object because
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
let fnToCall = handleNoUsers;
let shouldEmail = false;

// If community users were passed in...
if (context.community && context.community.length > 0) {
// Default to either autoAdd or invite based on canAutoAddUser.
fnToCall = context.canAutoAddUser
? processAutoAddUsers
: processInviteUsers;
// If we have an email object
// Then we will auto add...
// But whether or not we email is still in question
if (context.email) {
// If the email object has the groupId property...
if (context.email.hasOwnProperty("groupId")) {
// If the email objects groupId property is the same as the current groupId in context...
// (This function is part of a flow that could work for N groupIds)
if (context.email.groupId === context.groupId) {
// Then we auto add and send email
fnToCall = processAutoAddUsers;
shouldEmail = true;
} // ELSE if the groupId's do NOT match, we will fall back
// To autoAdd or invite as per line 32.
// We are doing the above logic (lines 43 - 47) because
// We wish to add users to core groups, followers, and content groups
// but only to email the core group.
} else {
// If it does not have a groupId at all then we will autoAdd and email.
fnToCall = processAutoAddUsers;
shouldEmail = true;
}
}
}
// Return/call the function
return fnToCall(context, "community", shouldEmail);
}

/**
* @private
* Handles add/invite logic for Org users
* It returns either an empty instance of the addOrInviteResponse
* object, or either ther esponse from processing auto adding a users or inviting a user
*
* @export
* @param {IAddOrInviteContext} context context object
* @return {IAddOrInviteResponse} response object
*/
export async function addOrInviteOrgUsers(
context: IAddOrInviteContext
): Promise<IAddOrInviteResponse> {
// If there are no org users return handling no users
if (!context.org || context.org.length === 0) {
// we return an empty object because
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
return handleNoUsers();
}
// for org user if you have assignUsers then auto add the user
// if not then invite the user
return context.canAutoAddUser
? processAutoAddUsers(context, "org")
: processInviteUsers(context, "org");
}

/**
* @private
* Handles add/invite logic for partnered org users.
* It returns either an empty instance of the addOrInviteResponse
* object, or their response from inviting users.
*
* @export
* @param {IAddOrInviteContext} context context object
* @return {IAddOrInviteResponse} response object
*/
export async function addOrInvitePartneredUsers(
context: IAddOrInviteContext
): Promise<IAddOrInviteResponse> {
// If there are no org users return handling no users
if (!context.partnered || context.partnered.length === 0) {
// we return an empty object because
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
return handleNoUsers();
}
// process invite
return processInviteUsers(context, "partnered");
}

/**
* @private
* Handles add/invite logic for world users
* It either returns an empty instance of the add/invite response
* object, or a populated version from processInviteUsers
*
* @export
* @param {IAddOrInviteContext} context Context object
* @return {IAddOrInviteResponse} Response object
*/
export async function addOrInviteWorldUsers(
context: IAddOrInviteContext
): Promise<IAddOrInviteResponse> {
// If there are no world users return handling no users
if (!context.world || context.world.length === 0) {
// we return an empty object because
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
return handleNoUsers();
}
// process invite
return processInviteUsers(context, "world");
}

// Add or invite flow based on the type of user ends here

/**
* @private
* Returns an empty instance of the addorinviteresponse object.
* We are using this because if you leave out any of the props
* from the final object and you are concatting together arrays you can concat
* an undeifined inside an array which will throw off array lengths.
*
* @export
* @return {IAddOrInviteResponse}
*/
export async function handleNoUsers(
context?: IAddOrInviteContext,
userType?: "world" | "org" | "community" | "partnered",
shouldEmail?: boolean
): Promise<IAddOrInviteResponse> {
return {
notAdded: [],
notEmailed: [],
notInvited: [],
users: [],
errors: [],
};
}

/**
* @private
* Takes users array and sorts them into an object by the type of user they are
* based on the orgType prop (world|org|community)
*
* @export
* @param {IUserWithOrgType[]} users array of users
* @return {IUserOrgRelationship} Object of users sorted by type (world, org, community)
*/
export function groupUsersByOrgRelationship(
users: IUserWithOrgType[]
): IUserOrgRelationship {
return users.reduce(
(acc, user) => {
// keyof needed to make bracket notation work without TS throwing a wobbly.
const orgType = user.orgType as keyof IUserOrgRelationship;
acc[orgType].push(user);
return acc;
},
{
world: [],
org: [],
community: [],
partnered: [],
collaborationCoordinator: [],
}
);
}
33 changes: 33 additions & 0 deletions packages/common/src/groups/_internal/autoAddUsersAsAdmins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
IAddGroupUsersResult,
IUser,
addGroupUsers,
} from "@esri/arcgis-rest-portal";
import { IAuthenticationManager } from "@esri/arcgis-rest-request";

/**
* @private
* Auto add N users to a single group, with users added as admins of that group
*
* @export
* @param {string} id Group ID
* @param {IUser[]} admins array of users to add to group as admin
* @param {IAuthenticationManager} authentication authentication manager
* @return {IAddGroupUsersResult} Result of the transaction (null if no users are passed in)
*/
export function autoAddUsersAsAdmins(
id: string,
admins: IUser[],
authentication: IAuthenticationManager
): Promise<IAddGroupUsersResult | null> {
let response = Promise.resolve(null);
if (admins.length) {
const args = {
id,
admins: admins.map((a) => a.username),
authentication,
};
response = addGroupUsers(args);
}
return response;
}
82 changes: 82 additions & 0 deletions packages/common/src/groups/_internal/processAutoAddUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { IUser } from "@esri/arcgis-rest-types";
import { IAddOrInviteContext, IAddOrInviteResponse } from "../types";
import { getProp } from "../../objects/get-prop";
import { ArcGISRequestError } from "@esri/arcgis-rest-request";
import { autoAddUsers } from "../autoAddUsers";
import { processEmailUsers } from "./processEmailUsers";
import { autoAddUsersAsAdmins } from "./autoAddUsersAsAdmins";

/**
* @private
* Governs logic for automatically adding N users to a group.
* Users are added as either a regular user OR as an administrator of the group
* depending on the addUserAsGroupAdmin prop on the IAddOrInviteContext.
* If there is an email object on the IAddOrInviteContext, then email notifications are sent.
*
* @export
* @param {IAddOrInviteContext} context context object
* @param {string} userType what type of user is it: org | world | community
* @param {boolean} [shouldEmail=false] should the user be emailed?
* @return {IAddOrInviteResponse} response object
*/
export async function processAutoAddUsers(
context: IAddOrInviteContext,
userType:
| "world"
| "org"
| "community"
| "partnered"
| "collaborationCoordinator",
shouldEmail: boolean = false
): Promise<IAddOrInviteResponse> {
// fetch users out of context object
const users: IUser[] = getProp(context, userType);
let autoAddResponse;
let emailResponse;
let notAdded: string[] = [];
let errors: ArcGISRequestError[] = [];
// fetch addUserAsGroupAdmin out of context
const { addUserAsGroupAdmin } = context;

if (addUserAsGroupAdmin) {
// if is core group we elevate user to admin
autoAddResponse = await autoAddUsersAsAdmins(
getProp(context, "groupId"),
users,
getProp(context, "primaryRO")
);
} else {
// if not then we are just auto adding them
autoAddResponse = await autoAddUsers(
getProp(context, "groupId"),
users,
getProp(context, "primaryRO")
);
}
// handle notAdded users
if (autoAddResponse.notAdded) {
notAdded = notAdded.concat(autoAddResponse.notAdded);
}
// Merge errors into empty array
if (autoAddResponse.errors) {
errors = errors.concat(autoAddResponse.errors);
}
// run email process
if (shouldEmail) {
emailResponse = await processEmailUsers(context);
// merge errors in to overall errors array to keep things flat
if (emailResponse.errors && emailResponse.errors.length > 0) {
errors = errors.concat(emailResponse.errors);
}
}
// if you leave out any of the props
// from the final object and you are concatting together arrays you can concat
// an undeifined inside an array which will throw off array lengths.
return {
users: users.map((u) => u.username),
notAdded,
errors,
notEmailed: emailResponse?.notEmailed || [],
notInvited: [],
};
}
Loading