From 603fd03145efb65b379f262dbe352926b4ded540 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Fri, 10 Jul 2026 23:02:47 -0700 Subject: [PATCH 1/2] feat: add candidate search dropdown --- apps/admin_dashboard/src/main.tsx | 132 ++++++++++++++++-- .../integration/test_dashboard_playwright.py | 24 +++- 2 files changed, 138 insertions(+), 18 deletions(-) diff --git a/apps/admin_dashboard/src/main.tsx b/apps/admin_dashboard/src/main.tsx index c9d0ca08..e7d5d63e 100644 --- a/apps/admin_dashboard/src/main.tsx +++ b/apps/admin_dashboard/src/main.tsx @@ -1802,7 +1802,7 @@ function App() { async function addGigApplication(gigId: string, crmProfile: string) { const normalizedProfile = crmProfile.trim() if (!normalizedProfile) { - showToast("Paste a CRM Contact profile first", "warning") + showToast("Choose a candidate first", "warning") return false } setBusy(`gig:${gigId}:addCandidate`, true) @@ -5423,6 +5423,7 @@ function GigsView(props: { <> {filterBar} Promise onUpdateApplicationStatus: (gigId: string, applicationId: string, status: string) => void }) { - const [crmProfile, setCrmProfile] = useState("") + const [candidateQuery, setCandidateQuery] = useState("") + const [candidateMatches, setCandidateMatches] = useState([]) + const [selectedCandidate, setSelectedCandidate] = useState(null) + const [candidateSearchError, setCandidateSearchError] = useState("") const applications = Array.isArray(gig.applications) ? gig.applications : [] const isRecruiting = gig.status === "recruiting" + const candidateQueryReady = candidateQuery.trim().length >= 2 + const selectedCandidateId = selectedCandidate?.crm_contact_id || "" + const candidateProfile = selectedCandidateId + ? crmContactUrl(selectedCandidateId) || selectedCandidateId + : "" const threadUrl = gig.discord_guild_id && gig.discord_thread_id ? `https://discord.com/channels/${encodeURIComponent( @@ -6018,6 +6031,51 @@ function GigDetailPage({ )}/${encodeURIComponent(gig.discord_thread_id)}` : "" const staleAge = staleRecruitingAge(gig, staleDays) + + useEffect(() => { + if (!canWrite) return + const query = candidateQuery.trim() + if (selectedCandidate && query === personSearchLabel(selectedCandidate)) { + setCandidateMatches([]) + setCandidateSearchError("") + return + } + if (query.length < 2) { + setCandidateMatches([]) + setCandidateSearchError("") + return + } + + const controller = new AbortController() + const timer = window.setTimeout(() => { + const params = new URLSearchParams({ limit: "8", query }) + void requestJson(`/dashboard/api/people?${params.toString()}`, { + signal: controller.signal, + }) + .then((people) => { + setCandidateMatches(people.filter((person) => Boolean(person.crm_contact_id))) + setCandidateSearchError("") + }) + .catch((error) => { + if (error instanceof DOMException && error.name === "AbortError") return + setCandidateMatches([]) + setCandidateSearchError(messageFromUnknown(error, "Unable to search candidates")) + }) + }, 300) + + return () => { + controller.abort() + window.clearTimeout(timer) + } + }, [candidateQuery, canWrite, selectedCandidate]) + + function chooseCandidate(person: Person) { + setSelectedCandidate(person) + setCandidateQuery(personSearchLabel(person)) + setCandidateMatches([]) + setCandidateSearchError("") + } + return (
{ event.preventDefault() - void onAddApplication(gig.id, crmProfile).then((added) => { - if (added) setCrmProfile("") + void onAddApplication(gig.id, candidateProfile).then((added) => { + if (added) { + setCandidateQuery("") + setCandidateMatches([]) + setSelectedCandidate(null) + } }) }} > - +
+ + {candidateQueryReady && !selectedCandidate ? ( +
+ {candidateSearchError ? ( +
{candidateSearchError}
+ ) : candidateMatches.length ? ( + candidateMatches.map((person) => { + const label = personSearchLabel(person) + const detail = [person.email_508 || person.email, person.contact_type] + .filter(Boolean) + .join(" | ") + return ( + + ) + }) + ) : ( +
+ No candidates match this search +
+ )} +
+ ) : null} +
+ ) + }) + ) : ( +
+ No candidates match this search +
+ )} +
+ ) : null} + + ) : ( + - {candidateQueryReady && !selectedCandidate ? ( -
- {candidateSearchError ? ( -
{candidateSearchError}
- ) : candidateMatches.length ? ( - candidateMatches.map((person) => { - const label = personSearchLabel(person) - const detail = [person.email_508 || person.email, person.contact_type] - .filter(Boolean) - .join(" | ") - return ( - - ) - }) - ) : ( -
- No candidates match this search -
- )} -
- ) : null} - + )}