fix(mail): search every owned job
CI and Deploy / test (pull_request) Successful in 5m13s
CI and Deploy / deploy (pull_request) Has been skipped

Replace ineffective pageSize=100 selectors with a bounded owner-filtered search endpoint for composing and moving linked threads.
This commit is contained in:
cesnimda
2026-08-15 20:13:14 +02:00
parent dc511296a4
commit a08ba9b45d
9 changed files with 167 additions and 57 deletions
@@ -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>