fix(mail): search every owned job
Replace ineffective pageSize=100 selectors with a bounded owner-filtered search endpoint for composing and moving linked threads.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
@@ -43,7 +44,6 @@ import {
|
||||
GmailStatus,
|
||||
GmailThreadRefreshResult,
|
||||
GmailUnlinkResult,
|
||||
JobApplication,
|
||||
} from "../types";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
@@ -103,9 +103,7 @@ function formatReasonLabel(label: string) {
|
||||
}
|
||||
}
|
||||
|
||||
interface PagedResult<T> {
|
||||
items: T[];
|
||||
}
|
||||
type JobChoice = { id: number; jobTitle: string; companyName: string };
|
||||
|
||||
export type CorrespondenceJobContext = {
|
||||
companyName?: string | null;
|
||||
@@ -153,7 +151,9 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
const [linkedThreadRefreshLoading, setLinkedThreadRefreshLoading] = useState(false);
|
||||
const [importingMessageId, setImportingMessageId] = useState<string | null>(null);
|
||||
const [importingThreadId, setImportingThreadId] = useState<string | null>(null);
|
||||
const [availableJobs, setAvailableJobs] = useState<JobApplication[]>([]);
|
||||
const [availableJobs, setAvailableJobs] = useState<JobChoice[]>([]);
|
||||
const [jobChoiceQuery, setJobChoiceQuery] = useState("");
|
||||
const [jobChoicesLoading, setJobChoicesLoading] = useState(false);
|
||||
const [manageThreadId, setManageThreadId] = useState<string | null>(null);
|
||||
const [manageTargetJobId, setManageTargetJobId] = useState<number>(jobId);
|
||||
const [manageNote, setManageNote] = useState("");
|
||||
@@ -195,12 +195,15 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
}
|
||||
}, [jobId, toast]);
|
||||
|
||||
const loadAvailableJobs = useCallback(async () => {
|
||||
const loadAvailableJobs = useCallback(async (query?: string) => {
|
||||
try {
|
||||
const res = await api.get<PagedResult<JobApplication>>("/jobapplications", { params: { page: 1, pageSize: 100, sortBy: "dateApplied", sortDir: "desc" } });
|
||||
setAvailableJobs((res.data?.items ?? []).filter((item) => item.id !== jobId));
|
||||
setJobChoicesLoading(true);
|
||||
const res = await api.get<JobChoice[]>("/jobapplications/choices", { params: { q: query?.trim() || undefined, limit: 50 } });
|
||||
setAvailableJobs((res.data ?? []).filter((item) => item.id !== jobId));
|
||||
} catch {
|
||||
setAvailableJobs([]);
|
||||
} finally {
|
||||
setJobChoicesLoading(false);
|
||||
}
|
||||
}, [jobId]);
|
||||
|
||||
@@ -257,8 +260,13 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
|
||||
useEffect(() => {
|
||||
void loadGmailStatus();
|
||||
void loadAvailableJobs();
|
||||
}, [loadAvailableJobs, loadGmailStatus]);
|
||||
}, [loadGmailStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manageThreadId) return;
|
||||
const timer = window.setTimeout(() => void loadAvailableJobs(jobChoiceQuery), jobChoiceQuery.trim() ? 250 : 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [jobChoiceQuery, loadAvailableJobs, manageThreadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gmailStatus?.connected || linkedThreadIds.length === 0) {
|
||||
@@ -405,6 +413,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
const openManageThread = (threadId: string) => {
|
||||
setManageThreadId(threadId);
|
||||
setManageTargetJobId(jobId);
|
||||
setJobChoiceQuery("");
|
||||
setManageNote("");
|
||||
};
|
||||
|
||||
@@ -443,7 +452,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
await loadGmailMatches(gmailQuery);
|
||||
setManageThreadId(null);
|
||||
const targetJob = availableJobs.find((item) => item.id === manageTargetJobId);
|
||||
toast(`Moved thread to ${targetJob?.company?.name || targetJob?.jobTitle || `job ${res.data.jobApplicationId}`}.`, "success");
|
||||
toast(`Moved thread to ${targetJob?.companyName || targetJob?.jobTitle || `job ${res.data.jobApplicationId}`}.`, "success");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, "Failed to move the Gmail thread."), "error");
|
||||
} finally {
|
||||
@@ -561,21 +570,18 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
|
||||
minRows={2}
|
||||
placeholder="Why this thread should stay in review or move to another job."
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Move to job</InputLabel>
|
||||
<Select
|
||||
value={String(manageTargetJobId)}
|
||||
label="Move to job"
|
||||
onChange={(event) => setManageTargetJobId(Number(event.target.value))}
|
||||
>
|
||||
<MenuItem value={String(jobId)}>Keep on current job</MenuItem>
|
||||
{availableJobs.map((item) => (
|
||||
<MenuItem key={item.id} value={String(item.id)}>
|
||||
{item.company?.name || "Unknown company"} • {item.jobTitle}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Autocomplete
|
||||
options={availableJobs}
|
||||
loading={jobChoicesLoading}
|
||||
value={availableJobs.find((item) => item.id === manageTargetJobId) ?? null}
|
||||
onInputChange={(_, value, reason) => {
|
||||
if (reason === "input" || reason === "clear") setJobChoiceQuery(value);
|
||||
}}
|
||||
onChange={(_, value) => setManageTargetJobId(value?.id ?? jobId)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(item) => `${item.companyName || "Unknown company"} • ${item.jobTitle}`}
|
||||
renderInput={(params) => <TextField {...params} label="Move to job" placeholder="Search every job" />}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setManageThreadId(null)} disabled={manageSaving}>Close</Button>
|
||||
|
||||
Reference in New Issue
Block a user