fix(discovery): expose saved-search load failures and retry

This commit is contained in:
cesnimda
2026-10-09 15:01:13 +02:00
parent bcc9599049
commit 2753d03db6
4 changed files with 379 additions and 7 deletions
+23 -6
View File
@@ -44,6 +44,8 @@ export default function JobDiscoveryPage() {
const [lastSearch, setLastSearch] = useState({ query: "", location: "" });
const [sortOrder, setSortOrder] = useState<SortOrder>("updated");
const [savedSearches, setSavedSearches] = useState<SavedSearch[]>([]);
const [savedSearchState, setSavedSearchState] = useState<"loading" | "loaded" | "error">("loading");
const [savedSearchLoad, setSavedSearchLoad] = useState(0);
const [savedName, setSavedName] = useState("");
const [savingSearch, setSavingSearch] = useState(false);
const [activeSavedSearchId, setActiveSavedSearchId] = useState<number | null>(null);
@@ -51,11 +53,22 @@ export default function JobDiscoveryPage() {
useEffect(() => {
let active = true;
setSavedSearchState("loading");
api.get<SavedSearch[]>("/job-discovery/saved-searches")
.then((response) => { if (active) setSavedSearches(Array.isArray(response.data) ? response.data : []); })
.catch(() => { /* One-off discovery remains usable if saved-search storage is unavailable. */ });
.then((response) => {
const rows = response.data;
if (!Array.isArray(rows) || !rows.every((row) => row && Number.isInteger(row.id) && row.id > 0
&& typeof row.name === "string" && typeof row.query === "string" && typeof row.location === "string"
&& typeof row.isActive === "boolean" && Number.isInteger(row.resultCount) && row.resultCount >= 0
&& Number.isInteger(row.dismissedCount) && row.dismissedCount >= 0
&& (row.lastRunAtUtc == null || typeof row.lastRunAtUtc === "string"))) {
throw new Error("Invalid saved-search response");
}
if (active) { setSavedSearches(rows); setSavedSearchState("loaded"); }
})
.catch(() => { if (active) setSavedSearchState("error"); });
return () => { active = false; };
}, []);
}, [savedSearchLoad]);
const runSearch = async (nextQuery: string, nextLocation: string) => {
setLoading(true); setError(""); setJobs([]); setHasSearched(true); setLastSearch({ query: nextQuery, location: nextLocation });
@@ -139,7 +152,7 @@ export default function JobDiscoveryPage() {
<TextField label={t("jobDiscoveryMunicipality")} value={location} onChange={(event) => setLocation(event.target.value)} />
<Button type="submit" variant="contained" startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <SearchIcon />} disabled={loading}>{t("jobDiscoverySearchNav")}</Button>
</Box>
<Card variant="outlined">
<Card variant="outlined" aria-busy={savedSearchState === "loading"}>
<CardContent sx={{ display: "grid", gap: 1.5 }}>
<Box>
<Typography component="h2" variant="h6" sx={{ fontWeight: 800 }}>{t("jobDiscoverySavedSearches")}</Typography>
@@ -147,9 +160,13 @@ export default function JobDiscoveryPage() {
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "minmax(0, 1fr) auto" }, gap: 1 }}>
<TextField size="small" label={t("jobDiscoverySavedName")} value={savedName} onChange={(event) => setSavedName(event.target.value)} />
<Button variant="outlined" startIcon={<BookmarkAddOutlinedIcon />} onClick={() => void saveSearch()} disabled={savingSearch}>{t("jobDiscoverySaveSearch")}</Button>
<Button variant="outlined" startIcon={<BookmarkAddOutlinedIcon />} onClick={() => void saveSearch()} disabled={savingSearch || savedSearchState !== "loaded"}>{t("jobDiscoverySaveSearch")}</Button>
</Box>
{savedSearches.length === 0 ? <Typography variant="body2" color="text.secondary">{t("jobDiscoveryNoSavedSearches")}</Typography> : (
{savedSearchState === "loading" ? <Typography role="status" aria-label={t("jobDiscoverySavedLoading")} variant="body2" color="text.secondary">{t("jobDiscoverySavedLoading")}</Typography> : savedSearchState === "error" ? (
<Alert severity="error" variant="outlined" action={<Button color="inherit" onClick={() => setSavedSearchLoad((current) => current + 1)}>{t("retry")}</Button>}>
{t("jobDiscoverySavedLoadFailed")}
</Alert>
) : savedSearches.length === 0 ? <Typography variant="body2" color="text.secondary">{t("jobDiscoveryNoSavedSearches")}</Typography> : (
<Stack spacing={1}>
{savedSearches.map((saved) => (
<Box key={saved.id} sx={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1, p: 1.25, border: 1, borderColor: activeSavedSearchId === saved.id ? "primary.main" : "divider", borderRadius: 2 }}>