Skip to content

Add candidate search dropdown#384

Merged
michaelmwu merged 3 commits into
mainfrom
michaelmwu/candidate-dropdown-search
Jul 11, 2026
Merged

Add candidate search dropdown#384
michaelmwu merged 3 commits into
mainfrom
michaelmwu/candidate-dropdown-search

Conversation

@michaelmwu

@michaelmwu michaelmwu commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the pasted CRM profile field in gig People management with a debounced candidate search dropdown
  • require an explicit candidate selection before enabling the add action, while preserving the CRM-verified application flow
  • cover the search-select-add workflow in the Playwright dashboard integration test

Why

Adding a candidate required copying and pasting a CRM profile URL, which made a common dashboard action slower and error-prone. The dashboard already has a permission-checked people search endpoint, so the UI can select an existing CRM candidate directly.

Validation

  • bun run check (admin dashboard)
  • uv run pytest tests/integration/test_dashboard_playwright.py -q
  • pre-commit: ruff and ruff-format

Note

Medium Risk
Changes who can add candidates how and what value is sent as crm_profile, but reuses the existing applications API; mis-selection or permission gaps could add the wrong CRM contact.

Overview
Gig detail — add candidate now branches on people:read: users with that permission get a debounced search against /dashboard/api/people (min 2 chars, dropdown, explicit selection) and submit the resolved CRM contact URL/ID via the existing onAddApplication / crm_profile POST. Users without search permission still use the manual CRM profile input.

Wiring adds canSearchCandidates from the gigs view into GigDetailPage, resets add-candidate UI state after success, sets key={gig.id} on the detail page, and updates the empty-selection toast to “Choose a candidate first.” Built dashboard assets are refreshed in the API static manifest.

Reviewed by Cursor Bugbot for commit 7715160. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added candidate search and autocomplete when adding people to a gig.
    • Candidates can be selected from CRM records instead of entering a profile URL.
    • Search results now display clear candidate labels and handle search errors.
  • Bug Fixes

    • Updated validation messaging to prompt users to choose a candidate.
    • Candidate selection resets correctly after a successful addition.

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_26bef785-67fb-4623-acbc-8112dce2f356)

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The gig detail flow now searches CRM-linked people records, lets users select a candidate, and submits the selected CRM profile when adding an application. Playwright coverage mocks query-based people search and verifies the Devon candidate selection flow.

Changes

Candidate search and selection

Layer / File(s) Summary
Dashboard candidate search flow
apps/admin_dashboard/src/main.tsx
GigDetailPage uses debounced people search, candidate selection, CRM profile derivation, autocomplete results, and updated application submission state.
Search interaction validation
tests/integration/test_dashboard_playwright.py
The mocked people endpoint handles query parameters, returns a Devon candidate, and the Playwright flow searches for and selects that candidate.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a candidate search dropdown in the gig flow.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch michaelmwu/candidate-dropdown-search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/admin_dashboard/src/main.tsx`:
- Around line 6049-6063: Update the error handling in the candidate search
promise within the debounced request callback to recognize aborts wrapped by
requestJson in ApiRequestError, not only direct DOMException AbortError values.
Preserve the early return for cancellations, and only clear matches and set
candidateSearchError for genuine request failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aac15f7c-4da2-4618-b269-2e0897ce0868

📥 Commits

Reviewing files that changed from the base of the PR and between c57cd79 and 603fd03.

📒 Files selected for processing (2)
  • apps/admin_dashboard/src/main.tsx
  • tests/integration/test_dashboard_playwright.py

Comment thread apps/admin_dashboard/src/main.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 603fd03145

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/admin_dashboard/src/main.tsx Outdated
value={candidateQuery}
autoComplete="off"
placeholder="Search by name or email"
aria-label="Search candidates to add"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Commit the rebuilt dashboard bundle

In this repo the FastAPI app serves the checked-in bundle from apps/api/src/five08/backend/static/dashboard; I checked the committed index-LL0oAOC_.js and it still has CRM profile for candidate/Paste a CRM, not this new search label. With only the source updated, production and test_dashboard_playwright.py (which starts the FastAPI app, not Vite) continue to run the old UI, so the new search-select flow is unavailable and the updated Playwright step cannot pass until the generated static assets are committed.

Useful? React with 👍 / 👎.

const controller = new AbortController()
const timer = window.setTimeout(() => {
const params = new URLSearchParams({ limit: "8", query })
void requestJson<Person[]>(`/dashboard/api/people?${params.toString()}`, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve candidate adds for gig-only members

For Discord Member sessions, /dashboard/api/me grants gigs:read/gigs:write but not people:read, and dashboard_people_handler requires people:read. Since this new search always calls /dashboard/api/people and the form disables submit until a result is selected, gig owners who could previously paste a CRM URL and pass dashboard_add_gig_application_handler can no longer add candidates; the search returns 403 and no candidateProfile is ever set. Gate the search UI on people:read with a fallback or use a gigs-write candidate lookup.

Useful? React with 👍 / 👎.

Comment thread apps/admin_dashboard/src/main.tsx Outdated
Comment on lines +6212 to +6215
onChange={(event) => {
setCandidateQuery(event.target.value)
setSelectedCandidate(null)
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale candidate matches when the query changes

When a user edits the search after a previous query returned matches, the old candidateMatches remain until the debounced request completes. If the old result set had exactly one entry and the user presses Enter (or clicks the still-visible stale option) before the new search returns, chooseCandidate(candidateMatches[0]) selects and enables adding the previous person even though the input contains a different query. Clear matches/errors on query changes or tie matches to the query they were fetched for.

Useful? React with 👍 / 👎.

Comment thread apps/admin_dashboard/src/main.tsx Outdated
setCandidateSearchError("")
})
.catch((error) => {
if (error instanceof DOMException && error.name === "AbortError") return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore aborts from requestJson-wrapped searches

When a user changes the candidate query after a debounced fetch has already started, the cleanup aborts that fetch, but requestJson catches fetch failures and rethrows an ApiRequestError, so this DOMException check never matches. Normal typing can therefore surface an abort as a candidate search error and clear matches for the active query until the next request wins; check the controller signal or the wrapped error instead of only DOMException.

Useful? React with 👍 / 👎.

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_898f79b7-854e-4ee3-b6ca-963dd5094f5a)

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8fa36829-536d-4f16-ba68-191d85acc7b7)

@michaelmwu
michaelmwu added this pull request to the merge queue Jul 11, 2026
Merged via the queue into main with commit 9eb540f Jul 11, 2026
13 checks passed
@michaelmwu
michaelmwu deleted the michaelmwu/candidate-dropdown-search branch July 11, 2026 06:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7715160ffa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const candidateProfile = selectedCandidateId
? crmContactUrl(selectedCandidateId) || selectedCandidateId
: ""
const profileForAdd = canSearchCandidates ? candidateProfile : crmProfile.trim()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep manual CRM entry for unsynced contacts

Users with people:read now get profileForAdd only from selectedCandidate, so the CRM URL input is hidden for admins. When a CRM contact is new or otherwise not yet in the local people cache, /dashboard/api/people cannot return it because the lookup selects FROM people (apps/api/src/five08/backend/api.py:2204), even though dashboard_add_gig_application_handler still accepts a crm_profile and verifies it directly against Espo (apps/api/src/five08/backend/api.py:7082, apps/api/src/five08/backend/api.py:7099). In that unsynced-contact case an admin can no longer add a valid candidate; keep a manual CRM profile fallback alongside search.

Useful? React with 👍 / 👎.

Comment on lines +6088 to +6089
.then((people) => {
setCandidateMatches(people.filter((person) => Boolean(person.crm_contact_id)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard search results against stale queries

Fresh evidence: although the updated onChange clears matches, this fulfillment handler still commits whichever request resolves without checking controller.signal.aborted or that its query is still the current input. If a prior search response for one query completes after the user has typed another query, stale candidates can reappear under the new input and be selected/added before the newer request wins.

Useful? React with 👍 / 👎.


const controller = new AbortController()
const timer = window.setTimeout(() => {
const params = new URLSearchParams({ limit: "8", query })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter inactive people out of candidate search

When the people cache contains missing_in_crm or conflict rows for a matching name, this typeahead omits the endpoint's sync_status=active filter, so stale CRM IDs can be offered as addable candidates; selecting one then reaches the add handler's live Espo lookup and fails with crm_profile_not_found (apps/api/src/five08/backend/api.py:7099). If enough stale rows match, they can also consume the 8-result limit before the valid active contact is shown, so pass the active filter for this picker.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant