Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/workflowQueries.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/workflowQueries.ts |
| Original SHA-256 | 5fce977fd13fe84a4ebcea7e35c5812f699a954ea56299cb468088372bc69447 |
| Git blob SHA | eccc95612c3f2db7461b87d4d4d58061374137a7 |
| Reading-copy SHA-256 | 5fce977fd13fe84a4ebcea7e35c5812f699a954ea56299cb468088372bc69447 |
Recovered source transcript
Download complete source text ↓196 lines · 7.2 KB
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { workflowApi, type CandidateQuery } from './workflow'
import type { CandidateSearchRow } from './workflow'
// Query keys are namespaced so a bulk action can invalidate everything a candidate appears in with one call.
// A bulk owner assignment changes the bench, the lists, and the desks at once, and a screen still showing
// the old owner after the action reported success is how a recruiter stops trusting the button.
const K = {
search: (q: CandidateQuery) => ['candidates', 'search', q] as const,
saved: ['candidates', 'saved-searches'] as const,
lists: ['candidates', 'lists'] as const,
listsFor: (id: string) => ['candidates', id, 'lists'] as const,
docs: (id: string) => ['candidates', id, 'documents'] as const,
messages: (id: string) => ['candidates', id, 'messages'] as const,
work: (t: string, id: string) => ['work', t, id] as const,
myClaims: ['work', 'mine'] as const,
templates: ['message-templates'] as const,
duplicates: ['candidates', 'duplicates'] as const,
}
export function useCandidateSearch(q: CandidateQuery, enabled = true) {
return useQuery({
queryKey: K.search(q),
queryFn: () => workflowApi.search<CandidateSearchRow>(q),
enabled,
// A search is a question about right now. Keeping a previous answer while a new one loads stops the
// table flashing empty between keystrokes, which reads as "no results" and is the single most common
// way a search feels broken.
placeholderData: (prev) => prev,
})
}
export function useSavedSearches() {
return useQuery({ queryKey: K.saved, queryFn: workflowApi.savedSearches })
}
export function useSaveSearch() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { name: string; criteria: CandidateQuery; visibility: 'private' | 'shared'; pinned: boolean }) =>
workflowApi.saveSearch(v.name, v.criteria, v.visibility, v.pinned),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.saved }),
})
}
export function useDeleteSavedSearch() {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: string) => workflowApi.deleteSavedSearch(id),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.saved }),
})
}
export function useLists() {
return useQuery({ queryKey: K.lists, queryFn: workflowApi.lists })
}
export function useListsForCandidate(id: string) {
return useQuery({ queryKey: K.listsFor(id), queryFn: () => workflowApi.listsForCandidate(id), enabled: !!id })
}
export function useCreateList() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { name: string; purpose: string; description?: string }) =>
workflowApi.createList(v.name, v.purpose, v.description ?? ''),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.lists }),
})
}
export function useAddToList() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { listId: string; ids: string[]; note?: string }) =>
workflowApi.addToList(v.listId, v.ids, v.note ?? ''),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: K.lists })
void qc.invalidateQueries({ queryKey: ['candidates'] })
},
})
}
export function useRemoveFromList() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { listId: string; ids: string[] }) => workflowApi.removeFromList(v.listId, v.ids),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: K.lists })
void qc.invalidateQueries({ queryKey: ['candidates'] })
},
})
}
export function useBulkAssignOwner() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { ids: string[]; ownerUserId: string | null }) =>
workflowApi.bulkAssignOwner(v.ids, v.ownerUserId),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['candidates'] }),
})
}
// useWorkActivity RECORDS THE VISIT as it reads. That is deliberate: presence only means anything if opening
// a record is what registers it, and asking a recruiter to press a button to say they are looking at
// something is asking them to do the thing the feature exists to avoid.
export function useWorkActivity(type: string, id: string, enabled = true) {
return useQuery({
queryKey: K.work(type, id),
queryFn: () => workflowApi.touch(type, id),
enabled: enabled && !!id,
// Presence goes stale quickly and is worth refetching while a record is open, so a colleague arriving
// shows up without a reload.
refetchInterval: 60_000,
staleTime: 30_000,
})
}
export function useClaim() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { type: string; id: string; activity: string; note: string; steal?: boolean }) =>
workflowApi.claim(v.type, v.id, v.activity, v.note, v.steal),
onSuccess: (_d, v) => {
void qc.invalidateQueries({ queryKey: K.work(v.type, v.id) })
void qc.invalidateQueries({ queryKey: K.myClaims })
},
})
}
export function useRelease() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { type: string; id: string }) => workflowApi.release(v.type, v.id),
onSuccess: (_d, v) => {
void qc.invalidateQueries({ queryKey: K.work(v.type, v.id) })
void qc.invalidateQueries({ queryKey: K.myClaims })
},
})
}
export function useMyClaims() {
return useQuery({ queryKey: K.myClaims, queryFn: workflowApi.myClaims })
}
export function useCandidateDocuments(id: string) {
return useQuery({ queryKey: K.docs(id), queryFn: () => workflowApi.documents(id), enabled: !!id })
}
export function useSupersedeDocument(candidateId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (docId: string) => workflowApi.supersedeDocument(docId),
onSuccess: () => void qc.invalidateQueries({ queryKey: K.docs(candidateId) }),
})
}
export function useCandidateMessages(id: string) {
return useQuery({ queryKey: K.messages(id), queryFn: () => workflowApi.candidateMessages(id), enabled: !!id })
}
export function useTemplates() {
return useQuery({ queryKey: K.templates, queryFn: workflowApi.templates })
}
export function useSendMessage(candidateId: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: workflowApi.sendMessage,
onSuccess: () => {
void qc.invalidateQueries({ queryKey: K.messages(candidateId) })
// A send also writes an activity row, so the record's history is stale too.
void qc.invalidateQueries({ queryKey: ['recruiting', 'activities'] })
},
})
}
export function useDuplicates() {
return useQuery({ queryKey: K.duplicates, queryFn: workflowApi.duplicates })
}
export function useMergeCandidates() {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { keepId: string; mergeId: string; reason: string }) =>
workflowApi.merge(v.keepId, v.mergeId, v.reason),
// A merge moves rows across roughly eighteen tables. Rather than enumerate them, everything candidate
// shaped is invalidated: it is one refetch on a rare action, against the alternative of a screen quietly
// showing a person who no longer exists.
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ['candidates'] })
void qc.invalidateQueries({ queryKey: ['work'] })
},
})
}