diff --git a/src/api/functions/organizations.ts b/src/api/functions/organizations.ts index 89de3650..bf067837 100644 --- a/src/api/functions/organizations.ts +++ b/src/api/functions/organizations.ts @@ -25,6 +25,7 @@ import { Modules } from "common/modules.js"; import { retryDynamoTransactionWithBackoff } from "api/utils.js"; import { Redis, ValidLoggers } from "api/types.js"; import { createLock, IoredisAdapter, type SimpleLock } from "redlock-universal"; +import { batchGetUserInfo } from "./uin.js"; export interface GetOrgInfoInputs { id: string; @@ -54,7 +55,7 @@ export async function getOrgInfo({ ConsistentRead: true, }); let response = { leads: [] } as { - leads: { name: string; username: string; title: string | undefined }[]; + leads: { name?: string; username: string; title: string | undefined }[]; }; try { const responseMarshall = await dynamoClient.send(query); @@ -98,18 +99,39 @@ export async function getOrgInfo({ .map( (x) => ({ - name: x.name, username: x.username, title: x.title, nonVotingMember: x.nonVotingMember || false, }) as { - name: string; username: string; title: string | undefined; nonVotingMember: boolean; }, ); - response = { ...response, leads: unmarshalledLeads }; + + // Resolve usernames to names + const emails = unmarshalledLeads.map((lead) => lead.username); + const userInfo = await batchGetUserInfo({ + emails, + dynamoClient, + logger, + }); + + // Add names to leads + const leadsWithNames = unmarshalledLeads.map((lead) => { + const info = userInfo[lead.username]; + const name = + info?.firstName || info?.lastName + ? [info.firstName, info.lastName].filter(Boolean).join(" ") + : undefined; + + return { + ...lead, + name, + }; + }); + + response = { ...response, leads: leadsWithNames }; } } catch (e) { if (e instanceof BaseError) { diff --git a/src/common/types/organizations.ts b/src/common/types/organizations.ts index ccce5ece..5fe24802 100644 --- a/src/common/types/organizations.ts +++ b/src/common/types/organizations.ts @@ -6,7 +6,7 @@ import { z } from "zod/v4"; export const orgLeadEntry = z.object({ name: z.optional(z.string()), username: z.email().refine( - (email) => email.endsWith('@illinois.edu'), + (email) => email.endsWith('@illinois.edu') || email.endsWith('@acm.illinois.edu'), { message: 'Email must be from the @illinois.edu domain' } ), title: z.optional(z.string()), @@ -25,7 +25,7 @@ export const orgLinkEntry = z.object({ url: z.url() }) -export const enforcedOrgLeadEntry = orgLeadEntry.extend({ name: z.string().min(1), title: z.string().min(1) }) +export const enforcedOrgLeadEntry = orgLeadEntry.extend({ title: z.string().min(1) }) export const getOrganizationInfoResponse = z.object({ id: z.enum(AllOrganizationNameList), diff --git a/src/ui/pages/organization/ManageOrganizationForm.test.tsx b/src/ui/pages/organization/ManageOrganizationForm.test.tsx index c8b21cce..4c9c6cf2 100644 --- a/src/ui/pages/organization/ManageOrganizationForm.test.tsx +++ b/src/ui/pages/organization/ManageOrganizationForm.test.tsx @@ -6,6 +6,7 @@ import { MantineProvider } from "@mantine/core"; import { notifications } from "@mantine/notifications"; import { ManageOrganizationForm } from "./ManageOrganizationForm"; import { MemoryRouter } from "react-router-dom"; +import { UserResolverProvider } from "@ui/components/NameOptionalCard"; // Mock the notifications module vi.mock("@mantine/notifications", () => ({ @@ -36,12 +37,14 @@ describe("ManageOrganizationForm Tests", () => { withCssVariables forceColorScheme="light" > - + + + , ); @@ -242,11 +245,13 @@ describe("ManageOrganizationForm Tests", () => { withCssVariables forceColorScheme="light" > - + + + , ); @@ -273,11 +278,13 @@ describe("ManageOrganizationForm Tests", () => { withCssVariables forceColorScheme="light" > - + + + , ); @@ -421,13 +428,15 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { withCssVariables forceColorScheme="light" > - + + + , ); @@ -443,17 +452,15 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); - expect(screen.getByText("jdoe@illinois.edu")).toBeInTheDocument(); - expect(screen.getByText("Jane Smith")).toBeInTheDocument(); - expect(screen.getByText("jsmith@illinois.edu")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); + expect(screen.getAllByText("jsmith@illinois.edu").length).toEqual(2); expect(screen.getByText("Vice Chair")).toBeInTheDocument(); }); const table = screen.getByRole("table"); - expect(table).toHaveTextContent("John Doe"); + expect(table).toHaveTextContent("jdoe@illinois.edu"); expect(table).toHaveTextContent("Chair"); - expect(table).toHaveTextContent("Jane Smith"); + expect(table).toHaveTextContent("jsmith@illinois.edu"); expect(table).toHaveTextContent("Vice Chair"); }); @@ -477,11 +484,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Fill in new lead form - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); await user.type( screen.getByLabelText("Lead Email"), "bwilson@illinois.edu", @@ -494,8 +500,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { // Check that the lead is queued await waitFor(() => { - expect(screen.getByText("Bob Wilson")).toBeInTheDocument(); - expect(screen.getByText("bwilson@illinois.edu")).toBeInTheDocument(); + expect(screen.getAllByText("bwilson@illinois.edu").length).toEqual(2); expect(screen.getByText("Treasurer")).toBeInTheDocument(); expect(screen.getByText("Queued for addition")).toBeInTheDocument(); }); @@ -508,11 +513,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Fill in form with invalid email - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); await user.type(screen.getByLabelText("Lead Email"), "invalid-email"); await user.type(screen.getByLabelText("Lead Title"), "Treasurer"); @@ -539,11 +543,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Try to add existing lead - await user.type(screen.getByLabelText("Lead Name"), "John Doe"); await user.type(screen.getByLabelText("Lead Email"), "jdoe@illinois.edu"); await user.type(screen.getByLabelText("Lead Title"), "Member"); @@ -569,10 +572,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); - // Find and click the Remove button for John Doe + // Find and click the Remove button for the first lead const removeButtons = screen.getAllByRole("button", { name: "Remove" }); await user.click(removeButtons[0]); @@ -588,7 +591,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Queue for removal @@ -605,7 +608,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await waitFor(() => { expect(screen.queryByText("Queued for removal")).not.toBeInTheDocument(); - expect(screen.getAllByText("Active").length).toBeGreaterThan(0); + expect(screen.getAllByText("Active").length).toEqual(2); }); }); @@ -615,11 +618,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Add a new lead - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); await user.type( screen.getByLabelText("Lead Email"), "bwilson@illinois.edu", @@ -628,7 +630,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await user.click(screen.getByRole("button", { name: "Add Lead" })); await waitFor(() => { - expect(screen.getByText("Bob Wilson")).toBeInTheDocument(); + expect(screen.getAllByText("bwilson@illinois.edu").length).toEqual(2); }); // Cancel the addition @@ -636,7 +638,9 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await user.click(cancelAddButton); await waitFor(() => { - expect(screen.queryByText("Bob Wilson")).not.toBeInTheDocument(); + expect( + screen.queryByText("bwilson@illinois.edu"), + ).not.toBeInTheDocument(); }); }); @@ -646,11 +650,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Add a new lead - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); await user.type( screen.getByLabelText("Lead Email"), "bwilson@illinois.edu", @@ -676,11 +679,10 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Add a new lead - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); await user.type( screen.getByLabelText("Lead Email"), "bwilson@illinois.edu", @@ -710,7 +712,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { expect(updateLeadsMock).toHaveBeenCalledWith( [ { - name: "Bob Wilson", + name: "", nonVotingMember: false, username: "bwilson@illinois.edu", title: "Treasurer", @@ -726,7 +728,7 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); const saveButton = screen.getByTestId("save-lead-changes"); @@ -739,22 +741,19 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Fill in and submit form - const nameInput = screen.getByLabelText("Lead Name"); const emailInput = screen.getByLabelText("Lead Email"); const titleInput = screen.getByLabelText("Lead Title"); - await user.type(nameInput, "Bob Wilson"); await user.type(emailInput, "bwilson@illinois.edu"); await user.type(titleInput, "Treasurer"); await user.click(screen.getByRole("button", { name: "Add Lead" })); // Check that fields are cleared await waitFor(() => { - expect(nameInput).toHaveValue(""); expect(emailInput).toHaveValue(""); expect(titleInput).toHaveValue(""); }); @@ -767,11 +766,14 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { await renderComponent(); await waitFor(() => { - expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getAllByText("jdoe@illinois.edu").length).toEqual(2); }); // Try to add without filling all fields - await user.type(screen.getByLabelText("Lead Name"), "Bob Wilson"); + await user.type( + screen.getByLabelText("Lead Email"), + "bwilson@illinois.edu", + ); await user.click(screen.getByRole("button", { name: "Add Lead" })); await waitFor(() => { @@ -797,11 +799,13 @@ describe("ManageOrganizationForm - Lead Management Tests", () => { withCssVariables forceColorScheme="light" > - + + + , ); diff --git a/src/ui/pages/organization/ManageOrganizationForm.tsx b/src/ui/pages/organization/ManageOrganizationForm.tsx index f73dcb22..2cd2f397 100644 --- a/src/ui/pages/organization/ManageOrganizationForm.tsx +++ b/src/ui/pages/organization/ManageOrganizationForm.tsx @@ -41,6 +41,7 @@ import { import { zod4Resolver as zodResolver } from "mantine-form-zod-resolver"; import * as z from "zod/v4"; import { ResponsiveTable, Column } from "@ui/components/ResponsiveTable"; +import { NameOptionalUserCard } from "@ui/components/NameOptionalCard"; type OrganizationData = z.infer; @@ -142,7 +143,7 @@ export const ManageOrganizationForm: React.FC = ({ }, [organizationId]); const handleAddLead = () => { - if (!newLeadName.trim() || !newLeadEmail.trim() || !newLeadTitle.trim()) { + if (!newLeadEmail.trim() || !newLeadTitle.trim()) { notifications.show({ title: "Invalid Input", message: "All fields are required to add a lead.", @@ -278,23 +279,13 @@ export const ManageOrganizationForm: React.FC = ({ label: "Lead", isPrimaryColumn: true, render: (lead) => ( - - -
- - - {lead.name} - - {lead.nonVotingMember && ( - - Non-Voting - - )} - - - {lead.username} - -
+ + + {lead.nonVotingMember && ( + + Non-Voting + + )} ), }, @@ -515,12 +506,6 @@ export const ManageOrganizationForm: React.FC = ({ )} - setNewLeadName(e.currentTarget.value)} - />