fix(i18n): unify shared view states
This commit is contained in:
@@ -53,7 +53,7 @@ export default function CompaniesTable() {
|
||||
},
|
||||
{
|
||||
initialData: [],
|
||||
errorMessage: t("companiesUpdateFailed"),
|
||||
errorMessage: t("companiesLoadFailed"),
|
||||
deps: [t],
|
||||
},
|
||||
);
|
||||
@@ -120,8 +120,8 @@ export default function CompaniesTable() {
|
||||
<ViewStateNotice
|
||||
loading={companiesResource.loading}
|
||||
error={companiesResource.error}
|
||||
title="Unable to load companies"
|
||||
description="The companies list is unavailable right now. Try again when the API is reachable."
|
||||
title={t("companiesLoadFailed")}
|
||||
description={t("companiesLoadFailedBody")}
|
||||
onRetry={companiesResource.reload}
|
||||
/>
|
||||
|
||||
|
||||
@@ -146,8 +146,8 @@ export default function DashboardView() {
|
||||
loadSummary,
|
||||
{
|
||||
initialData: { stats: null as JobStats | null, overview: null as OverviewAnalytics | null, reminderJobs: [] as ReminderJob[] },
|
||||
errorMessage: "Unable to load dashboard summary data right now.",
|
||||
deps: [],
|
||||
errorMessage: t("dashboardSummaryUnavailableTitle"),
|
||||
deps: [t],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -170,8 +170,8 @@ export default function DashboardView() {
|
||||
loadTrends,
|
||||
{
|
||||
initialData: { analytics: [] as AnalyticsPoint[], tags: [] as TagPoint[], tagTrends: null as TagTrendResponse | null },
|
||||
errorMessage: "Unable to load dashboard trends right now.",
|
||||
deps: [months],
|
||||
errorMessage: t("dashboardTrendsUnavailableTitle"),
|
||||
deps: [months, t],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -315,8 +315,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
},
|
||||
{
|
||||
initialData: { items: [], total: 0, page: 1, pageSize },
|
||||
errorMessage: "Unable to load jobs right now.",
|
||||
deps: [params, refreshToken, reloadToken, pageSize],
|
||||
errorMessage: t("jobTableJobsLoadFailed"),
|
||||
deps: [params, refreshToken, reloadToken, pageSize, t],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -614,15 +614,15 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
|
||||
<ViewStateNotice
|
||||
error={jobsResource.error}
|
||||
title={mode === "trash" ? "Unable to load trash" : "Unable to load jobs"}
|
||||
description={mode === "trash" ? "The deleted-jobs view cannot reach the API right now." : "The jobs list cannot reach the API right now."}
|
||||
title={mode === "trash" ? t("jobTableTrashLoadFailed") : t("jobTableJobsLoadFailed")}
|
||||
description={mode === "trash" ? t("jobTableTrashLoadFailedBody") : t("jobTableJobsLoadFailedBody")}
|
||||
onRetry={jobsResource.reload}
|
||||
/>
|
||||
{companiesError ? (
|
||||
<ViewStateNotice
|
||||
error={companiesError}
|
||||
title="Unable to load company filters"
|
||||
description="Company filter data is unavailable right now."
|
||||
title={t("jobTableCompanyFiltersLoadFailed")}
|
||||
description={t("jobTableCompanyFiltersLoadFailedBody")}
|
||||
onRetry={reloadCompanies}
|
||||
compact
|
||||
/>
|
||||
|
||||
@@ -102,8 +102,8 @@ export default function KanbanBoard() {
|
||||
},
|
||||
{
|
||||
initialData: [],
|
||||
errorMessage: "Unable to load the board right now.",
|
||||
deps: [],
|
||||
errorMessage: t("kanbanLoadFailed"),
|
||||
deps: [t],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -143,10 +143,10 @@ export default function KanbanBoard() {
|
||||
try {
|
||||
await api.patch(`/jobapplications/${movingJobId}/status`, { status });
|
||||
jobsResource.setData((prev) => prev.map((j) => (j.id === movingJobId ? { ...j, status } : j)));
|
||||
setAnnouncement(`Job moved to ${statusLabel(t, status)}.`);
|
||||
setAnnouncement(t("kanbanMoveSucceeded", { status: statusLabel(t, status) }));
|
||||
} catch (error: any) {
|
||||
setMoveError(getApiErrorMessage(error, "Unable to move this job right now."));
|
||||
setAnnouncement("Job move failed.");
|
||||
setMoveError(getApiErrorMessage(error, t("kanbanMoveFailed")));
|
||||
setAnnouncement(t("kanbanMoveFailedAnnouncement"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,10 +155,10 @@ export default function KanbanBoard() {
|
||||
try {
|
||||
await api.patch(`/jobapplications/${id}/status`, { status });
|
||||
jobsResource.setData((prev) => prev.map((j) => (j.id === id ? { ...j, status } : j)));
|
||||
setAnnouncement(`Job moved to ${statusLabel(t, status)}.`);
|
||||
setAnnouncement(t("kanbanMoveSucceeded", { status: statusLabel(t, status) }));
|
||||
} catch (error: any) {
|
||||
setMoveError(getApiErrorMessage(error, "Unable to move this job right now."));
|
||||
setAnnouncement("Job move failed.");
|
||||
setMoveError(getApiErrorMessage(error, t("kanbanMoveFailed")));
|
||||
setAnnouncement(t("kanbanMoveFailedAnnouncement"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,8 +187,8 @@ export default function KanbanBoard() {
|
||||
<ViewStateNotice
|
||||
loading={jobsResource.loading}
|
||||
error={jobsResource.error}
|
||||
title="Unable to load the kanban board"
|
||||
description="The board could not reach the API."
|
||||
title={t("kanbanLoadFailed")}
|
||||
description={t("kanbanLoadFailedBody")}
|
||||
onRetry={jobsResource.reload}
|
||||
/>
|
||||
|
||||
@@ -227,11 +227,11 @@ export default function KanbanBoard() {
|
||||
onBlur={() => { if (dragOverColumn === key) setDragOverColumn(null); }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape" && isDragActive) {
|
||||
event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement("Keyboard move cancelled.");
|
||||
event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement(t("kanbanKeyboardMoveCancelled"));
|
||||
} else if ((event.key === "Enter" || event.key === " ") && isDragActive) {
|
||||
event.preventDefault();
|
||||
if (droppable) void onDropTo(key as PipelineGroup);
|
||||
else setAnnouncement(`${label} is not a valid drop target.`);
|
||||
else setAnnouncement(t("kanbanInvalidDropTarget", { status: label }));
|
||||
}
|
||||
}}
|
||||
onDragEnter={() => { if (isDragActive) setDragOverColumn(key); }}
|
||||
@@ -291,19 +291,19 @@ export default function KanbanBoard() {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={dragJobId === j.id}
|
||||
aria-label={`${j.jobTitle}, ${statusLabel(t, j.status)}. ${dragJobId === j.id ? "Picked up; focus a column and press Enter to move." : "Press Space to pick up."}`}
|
||||
aria-label={t(dragJobId === j.id ? "kanbanCardPickedUpLabel" : "kanbanCardPickUpLabel", { title: j.jobTitle, status: statusLabel(t, j.status) })}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape" && dragJobId === j.id) {
|
||||
event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement("Keyboard move cancelled.");
|
||||
event.preventDefault(); setDragJobId(null); setDragOverColumn(null); setAnnouncement(t("kanbanKeyboardMoveCancelled"));
|
||||
} else if (event.key === " ") {
|
||||
event.preventDefault();
|
||||
const pickingUp = dragJobId !== j.id;
|
||||
setDragJobId(pickingUp ? j.id : null);
|
||||
setDragOverColumn(null);
|
||||
setAnnouncement(pickingUp ? `${j.jobTitle} picked up. Focus a column and press Enter to move.` : "Keyboard move cancelled.");
|
||||
setAnnouncement(pickingUp ? t("kanbanPickedUpAnnouncement", { title: j.jobTitle }) : t("kanbanKeyboardMoveCancelled"));
|
||||
}
|
||||
}}
|
||||
onDragStart={() => { setDragJobId(j.id); setAnnouncement(`${j.jobTitle} picked up.`); }}
|
||||
onDragStart={() => { setDragJobId(j.id); setAnnouncement(t("kanbanDragStartedAnnouncement", { title: j.jobTitle })); }}
|
||||
onDragEnd={() => { setDragJobId(null); setDragOverColumn(null); }}
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function RemindersView() {
|
||||
},
|
||||
{
|
||||
initialData: [],
|
||||
errorMessage: "Unable to load reminders right now.",
|
||||
deps: [],
|
||||
errorMessage: t("remindersLoadFailed"),
|
||||
deps: [t],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -113,8 +113,8 @@ export default function RemindersView() {
|
||||
<ViewStateNotice
|
||||
loading={remindersResource.loading}
|
||||
error={remindersResource.error}
|
||||
title="Unable to load reminders"
|
||||
description="The reminders view cannot reach the API right now."
|
||||
title={t("remindersLoadFailed")}
|
||||
description={t("remindersLoadFailedBody")}
|
||||
onRetry={remindersResource.reload}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
import { Alert, Box, Button, CircularProgress, Typography } from "@mui/material";
|
||||
|
||||
import type { ViewResourceError } from "../hooks/useViewResource";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
type Props = {
|
||||
loading?: boolean;
|
||||
@@ -14,11 +15,14 @@ type Props = {
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export default function ViewStateNotice({ loading = false, error = null, title, description, retryLabel = "Retry", onRetry, compact = false }: Props) {
|
||||
export default function ViewStateNotice({ loading = false, error = null, title, description, retryLabel, onRetry, compact = false }: Props) {
|
||||
const { t } = useI18n();
|
||||
const resolvedRetryLabel = retryLabel ?? t("retry");
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ py: compact ? 3 : 6, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress size={compact ? 24 : 28} />
|
||||
<Box role="status" aria-live="polite" sx={{ py: compact ? 3 : 6, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress size={compact ? 24 : 28} aria-label={t("loading")} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +30,7 @@ export default function ViewStateNotice({ loading = false, error = null, title,
|
||||
if (!error) return null;
|
||||
|
||||
const severity = error.kind === "unauthorized" ? "warning" : error.kind === "unavailable" ? "error" : "error";
|
||||
const detail = error.message !== title && error.message !== description ? error.message : null;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -35,11 +40,11 @@ export default function ViewStateNotice({ loading = false, error = null, title,
|
||||
alignItems: "flex-start",
|
||||
borderRadius: 3,
|
||||
}}
|
||||
action={error.retryable && onRetry ? <Button color="inherit" size="small" onClick={() => void onRetry()}>{retryLabel}</Button> : undefined}
|
||||
action={error.retryable && onRetry ? <Button color="inherit" size="small" onClick={() => void onRetry()}>{resolvedRetryLabel}</Button> : undefined}
|
||||
>
|
||||
<Typography sx={{ fontWeight: 800, mb: 0.35 }}>{title}</Typography>
|
||||
{description ? <Typography variant="body2">{description}</Typography> : null}
|
||||
{error.message ? <Typography variant="body2" sx={{ mt: 0.5 }}>{error.message}</Typography> : null}
|
||||
{detail ? <Typography variant="body2" sx={{ mt: 0.5 }}>{detail}</Typography> : null}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user