Skip to content

Commit

Permalink
feat(Projects): display user orgs in new team member search
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Sep 26, 2024
1 parent 7205428 commit 0886f89
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 17 deletions.
4 changes: 2 additions & 2 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
User,
UserSearchParams,
} from "./types";
import { CIDDescriptor, ProjectFileAuthor, ProjectTag } from "./types/Project";
import { AddableProjectTeamMember, CIDDescriptor, ProjectFileAuthor, ProjectTag } from "./types/Project";
import { Collection } from "./types/Collection";
import {
AuthorSearchParams,
Expand Down Expand Up @@ -562,7 +562,7 @@ class API {
page: params.page?.toString() || "1",
limit: params.limit?.toString() || "20",
});
const res = await axios.get<{ users: User[] } & ConductorBaseResponse>(
const res = await axios.get<{ users: AddableProjectTeamMember[] } & ConductorBaseResponse>(
`/project/${params.projectID}/team/addable?${queryParams}`
);
return res;
Expand Down
48 changes: 36 additions & 12 deletions client/src/components/projects/ManageTeamModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import axios from "axios";
import {
Modal,
Form,
Divider,
Button,
Icon,
ModalProps,
Loader,
List,
Image,
Dropdown,
Popup,
Expand All @@ -17,7 +15,7 @@ import {
TableProps,
Radio,
} from "semantic-ui-react";
import { CentralIdentityOrg, Project, User } from "../../types";
import { AddableProjectTeamMember, Project, User } from "../../types";
import {
isEmptyString,
sortUsersByName,
Expand All @@ -31,7 +29,6 @@ import { extractEmailDomain } from "../../utils/misc";
import api from "../../api";

type ProjectDisplayMember = User & { roleValue: string; roleDisplay: string };
type AddableUser = Pick<User, "uuid" | "firstName" | "lastName" | "avatar">;

interface ManageTeamModalProps extends ModalProps {
show: boolean;
Expand All @@ -56,8 +53,8 @@ const ManageTeamModal: React.FC<ManageTeamModalProps> = ({
const [loading, setLoading] = useState<boolean>(false);
const [hasNotSearched, setHasNotSearched] = useState<boolean>(true);
const [searchString, setSearchString] = useState<string>("");
const [includeOutsideOrg, setIncludeOutsideOrg] = useState<boolean>(false);
const [teamUserOptions, setTeamUserOptions] = useState<AddableUser[]>([]);
const [includeOutsideOrg, setIncludeOutsideOrg] = useState<boolean>(true);
const [teamUserOptions, setTeamUserOptions] = useState<AddableProjectTeamMember[]>([]);
const [teamUserOptsLoading, setTeamUserOptsLoading] =
useState<boolean>(false);

Expand Down Expand Up @@ -330,7 +327,18 @@ const ManageTeamModal: React.FC<ManageTeamModalProps> = ({
<Form onSubmit={(e) => e.preventDefault()} className="mt-16 h-72">
<Form.Field className="flex flex-col">
<div className="flex flex-row justify-between items-center mb-1">
<p className="text-xl font-semibold">Add Team Members</p>
<div className="flex flex-row items-center">
<p className="text-xl font-semibold">Add Team Members</p>
<Popup
content="Add users to the project team by searching for their name or email address. You can use the toggle switch to the right to restrict the search to users with the same email address domain as you."
trigger={
<Icon
name="question circle outline"
className=" !ml-1"
/>
}
/>
</div>
<div className="flex flex-row items-center">
<label className="" htmlFor="outside-org-radio">
Include users outside of{" "}
Expand Down Expand Up @@ -362,19 +370,35 @@ const ManageTeamModal: React.FC<ManageTeamModalProps> = ({
<Table celled compact>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={"8"}>Name</Table.HeaderCell>
<Table.HeaderCell width={"4"}>Name</Table.HeaderCell>
<Table.HeaderCell width={"4"}>Organization</Table.HeaderCell>
<Table.HeaderCell width={"2"}>Actions</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{teamUserOptsLoading && <Loader active inline="centered" />}
{teamUserOptsLoading && (
<Table.Row>
<Table.Cell colSpan={3}>
<Loader active inline="centered" />
</Table.Cell>
</Table.Row>
)}
{!teamUserOptsLoading &&
teamUserOptions.map((item) => (
<Table.Row key={item.uuid}>
<Table.Cell>
<Image avatar src={item.avatar} />
{item.firstName} {item.lastName}
</Table.Cell>
<Table.Cell>
{item.orgs && (
<p>
{item.orgs.slice(0, 3).map((org: { name: string }) => {
return truncateString(org.name, 45);
}).join(", ")}
</p>
)}
</Table.Cell>
<Table.Cell>
<Button
color="green"
Expand All @@ -394,7 +418,7 @@ const ManageTeamModal: React.FC<ManageTeamModalProps> = ({
!hasNotSearched &&
teamUserOptions.length === 0 && (
<Table.Row>
<Table.Cell colSpan={2}>
<Table.Cell colSpan={3}>
<p className="text-center">
No users found. Please try another search.
</p>
Expand All @@ -403,7 +427,7 @@ const ManageTeamModal: React.FC<ManageTeamModalProps> = ({
)}
{!teamUserOptsLoading && hasNotSearched && (
<Table.Row>
<Table.Cell colSpan={2}>
<Table.Cell colSpan={3}>
<p className="text-center">
Start typing to search for users to add to the
project.
Expand Down
6 changes: 5 additions & 1 deletion client/src/types/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export type ProjectFileWCustomData<
T extends keyof Project = never
> = ProjectFile & {
projectInfo: Record<K, string> &
(T extends never ? {} : ProjectFileWProjectData<T>);
(T extends never ? {} : ProjectFileWProjectData<T>);
};

export enum ProjectStatus {
Expand Down Expand Up @@ -160,3 +160,7 @@ export type Project = {
description?: string;
contentArea?: string;
};

export type AddableProjectTeamMember = Pick<User, "uuid" | "firstName" | "lastName" | "avatar"> & {
orgs: { name: string }[]
}
2 changes: 2 additions & 0 deletions client/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
ProjectModule,
ProjectModuleConfig,
ProjectModuleSettings,
AddableProjectTeamMember
} from "./Project";
import { User, Account, AuthorizedApp, UserWCentralID } from "./User";
import { Homework, AdaptAssignment } from "./Homework";
Expand Down Expand Up @@ -136,6 +137,7 @@ import {
} from "./HarvestRequest";

export type {
AddableProjectTeamMember,
AtlasSearchHighlight,
AssetFilters,
AssetFiltersAction,
Expand Down
27 changes: 27 additions & 0 deletions server/api/central-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,32 @@ async function getUserOrgs(
}
}

/**
* Get multiple users' organizations in a single request.
* Internal function, API requests should not call this directly.
*/
async function _getMultipleUsersOrgs(uuids: string[]): Promise<Record<string, { name: string }[]>> {
try {

if(!uuids || uuids.length === 0) {
return {};
}

const queryString = uuids.map(uuid => `uuids[]=${encodeURIComponent(uuid)}`).join('&');

const userRes = await useCentralIdentityAxios(false).get("/users/organizations" + "?" + queryString);

if (!userRes.data || !userRes.data.data) {
return {};
}

return userRes.data.data;
} catch (err) {
debugError(err);
return {};
}
}

async function addUserApplications(
req: TypedReqParamsAndBody<
{ id: string },
Expand Down Expand Up @@ -1113,6 +1139,7 @@ export default {
checkUserApplicationAccessInternal,
_getUserOrgsRaw,
getUserOrgs,
_getMultipleUsersOrgs,
addUserApplications,
deleteUserApplication,
addUserOrgs,
Expand Down
20 changes: 18 additions & 2 deletions server/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import authAPI from './auth.js';
import mailAPI from './mail.js';
import usersAPI from './users.js';
import alertsAPI from './alerts.js';
import centralIdentity from './central-identity.js';
import centralIdentityAPI from './central-identity.js';
import { getSubdomainFromLibrary } from '../util/librariesclient.js';
import projectFilesAPI from './projectfiles.js';
import ProjectFile from "../models/projectfile.js";
Expand Down Expand Up @@ -1681,8 +1681,24 @@ async function getAddableMembers(req, res) {
{ $limit: parsedLimit },
])


// Returns map of centralID to orgs (as {name: string} objects)
const orgsRes = await centralIdentityAPI._getMultipleUsersOrgs(users.map(u => u.centralID)).catch((e) => {
debugError(e); // fail silently
return {};
});

// attempt to match orgs to users
const usersWithOrgs = users.map((u) => {
const orgs = orgsRes[u.centralID] || [];
return {
...u,
orgs: orgs.map((o) => ({ name: o.name })),
};
});

return res.send({
users: users || [],
users: usersWithOrgs || [],
err: false,
});
} catch (e) {
Expand Down

0 comments on commit 0886f89

Please sign in to comment.