mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(charts): paint empty-window state instead of erasing the card
`health-chart.tsx` returned `null` when the loaded payload was empty in the selected range, which stripped the entire widget (header + tabs + chart). On the dashboard the user saw a missing tile with no explanation. Keep the card chrome mounted and paint a `<ChartEmptyState>` with distinct copy from the < 3-points "Mehr Messtage erforderlich" branch so the two situations don't blur. Adds `charts.noDataInRange*` strings across all six locales.
This commit is contained in:
1 parent
d02deb1769
commit
54fbbf8fc8
8 files changed
+120
-2
No files matched your search
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Erfasse mindestens 3 Einträge, um die Trendlinie freizuschalten.",
|
||||
"needMoreDistinctDaysTitle": "Mehr Messtage erforderlich",
|
||||
"needMoreDistinctDaysDescription": "Erfasse an mehreren Tagen, um eine Trendlinie freizuschalten.",
|
||||
"noDataInRangeTitle": "Keine Daten in diesem Zeitraum",
|
||||
"noDataInRangeDescription": "Für den gewählten Bereich liegen keine Messungen vor. Wechsle den Zeitraum oder erfasse einen neuen Wert.",
|
||||
"personalBaseline": "Dein Mittel",
|
||||
"deltaVsBaseline": "{delta} vs. dein Mittel",
|
||||
"deltaUnchanged": "entspricht deinem Mittel",
|
||||
|
||||
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Log at least 3 entries to unlock the trend line.",
|
||||
"needMoreDistinctDaysTitle": "More distinct days needed",
|
||||
"needMoreDistinctDaysDescription": "Log on more days to unlock the trend line.",
|
||||
"noDataInRangeTitle": "No data in this range",
|
||||
"noDataInRangeDescription": "No measurements fall inside the selected range. Switch the range or log a new measurement.",
|
||||
"personalBaseline": "Your normal",
|
||||
"deltaVsBaseline": "{delta} vs. your normal",
|
||||
"deltaUnchanged": "matches your normal",
|
||||
|
||||
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Registra al menos 3 entradas para desbloquear la línea de tendencia.",
|
||||
"needMoreDistinctDaysTitle": "Se necesitan más días distintos",
|
||||
"needMoreDistinctDaysDescription": "Registra en más días para desbloquear la línea de tendencia.",
|
||||
"noDataInRangeTitle": "Sin datos en este rango",
|
||||
"noDataInRangeDescription": "No hay mediciones en el rango seleccionado. Cambia el rango o registra una nueva medición.",
|
||||
"personalBaseline": "Tu media",
|
||||
"deltaVsBaseline": "{delta} vs. tu media",
|
||||
"deltaUnchanged": "coincide con tu media",
|
||||
|
||||
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Saisis au moins 3 entrées pour débloquer la courbe de tendance.",
|
||||
"needMoreDistinctDaysTitle": "Plus de jours distincts requis",
|
||||
"needMoreDistinctDaysDescription": "Saisis sur plusieurs jours pour débloquer la courbe de tendance.",
|
||||
"noDataInRangeTitle": "Aucune donnée sur cette période",
|
||||
"noDataInRangeDescription": "Aucune mesure ne tombe dans la plage sélectionnée. Change de plage ou saisis une nouvelle mesure.",
|
||||
"personalBaseline": "Ta moyenne",
|
||||
"deltaVsBaseline": "{delta} vs. ta moyenne",
|
||||
"deltaUnchanged": "correspond à ta moyenne",
|
||||
|
||||
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Registra almeno 3 voci per sbloccare la linea di tendenza.",
|
||||
"needMoreDistinctDaysTitle": "Servono più giorni distinti",
|
||||
"needMoreDistinctDaysDescription": "Registra in più giorni per sbloccare la linea di tendenza.",
|
||||
"noDataInRangeTitle": "Nessun dato in questo intervallo",
|
||||
"noDataInRangeDescription": "Nessuna misurazione rientra nell'intervallo selezionato. Cambia intervallo o registra una nuova misurazione.",
|
||||
"personalBaseline": "La tua media",
|
||||
"deltaVsBaseline": "{delta} vs. la tua media",
|
||||
"deltaUnchanged": "corrisponde alla tua media",
|
||||
|
||||
@@ -862,6 +862,8 @@
|
||||
"emptyStateDescription": "Zarejestruj co najmniej 3 wpisy, aby odblokować linię trendu.",
|
||||
"needMoreDistinctDaysTitle": "Potrzeba więcej różnych dni",
|
||||
"needMoreDistinctDaysDescription": "Rejestruj w więcej dni, aby odblokować linię trendu.",
|
||||
"noDataInRangeTitle": "Brak danych w tym zakresie",
|
||||
"noDataInRangeDescription": "Żadne pomiary nie mieszczą się w wybranym zakresie. Zmień zakres lub dodaj nowy pomiar.",
|
||||
"personalBaseline": "Twoja średnia",
|
||||
"deltaVsBaseline": "{delta} vs. twoja średnia",
|
||||
"deltaUnchanged": "odpowiada twojej średniej",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
/**
|
||||
* v1.4.43 W11-M6 — chart empty-window state.
|
||||
*
|
||||
* Pre-fix the chart returned `null` when the loaded data array was
|
||||
* empty in the selected range, erasing the entire card (header +
|
||||
* tabs + chart). The user saw a missing widget without explanation.
|
||||
* The fix paints a "no data in this range" empty state via
|
||||
* `<ChartEmptyState>` so the dashboard composition stays intact and
|
||||
* the user knows the chart loaded but the selected window holds no
|
||||
* measurements. The copy is distinct from the < 3-points "Mehr
|
||||
* Messtage erforderlich" branch so the two situations don't blur.
|
||||
*/
|
||||
|
||||
function buildData(rawCount: number): unknown[] {
|
||||
const points: unknown[] = [];
|
||||
Object.defineProperty(points, "rawCount", {
|
||||
value: rawCount,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
return points;
|
||||
}
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
isAuthenticated: true,
|
||||
user: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("<HealthChart> — empty-window state (W11-M6)", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("renders the no-data-in-range copy when data resolves to an empty array", async () => {
|
||||
const data = buildData(0);
|
||||
|
||||
vi.doMock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data, isLoading: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: () => Promise.resolve(),
|
||||
getQueryData: () => undefined,
|
||||
setQueryData: () => undefined,
|
||||
invalidateQueries: () => Promise.resolve(),
|
||||
}),
|
||||
useMutation: () => ({ mutate: () => undefined, isPending: false }),
|
||||
}));
|
||||
|
||||
const { I18nProvider } = await import("@/lib/i18n/context");
|
||||
const { HealthChart } = await import("../health-chart");
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="de">
|
||||
<HealthChart types={["PULSE"]} title="Pulse" unit="bpm" />
|
||||
</I18nProvider>,
|
||||
);
|
||||
|
||||
// The new empty state must paint…
|
||||
expect(html).toContain("Keine Daten in diesem Zeitraum");
|
||||
// …and the chart card chrome (title) must still be mounted so the
|
||||
// dashboard isn't missing a tile. The pre-fix `return null` would
|
||||
// have stripped this entirely.
|
||||
expect(html).toContain("Pulse");
|
||||
// The < 3-points branch must NOT paint here — empty-window and
|
||||
// sparse-data are different situations with distinct copy.
|
||||
expect(html).not.toContain("Mehr Messtage erforderlich");
|
||||
expect(html).not.toContain("Erfasse mindestens 3 Einträge");
|
||||
|
||||
vi.doUnmock("@tanstack/react-query");
|
||||
});
|
||||
});
|
||||
@@ -1132,7 +1132,14 @@ export function HealthChart({
|
||||
return map;
|
||||
}, [chartData, types]);
|
||||
|
||||
if (!isLoading && !data?.length) return null;
|
||||
// v1.4.43 W11-M6 — keep the card shell mounted even when the loaded
|
||||
// payload is empty. Pre-fix the early `return null` here erased the
|
||||
// whole widget (header + range tabs + chart) and the user saw an
|
||||
// unexplained gap on the dashboard. The in-card render branch below
|
||||
// now paints a "no data in this range" empty state so the user
|
||||
// understands the chart loaded but the selected window holds no
|
||||
// measurements — distinct copy from the < 3-points "more days
|
||||
// needed" hint so the two situations don't blur.
|
||||
|
||||
const maxPointIndex = Math.max(0, (chartData?.length ?? 1) - 1);
|
||||
|
||||
@@ -1288,7 +1295,29 @@ export function HealthChart({
|
||||
<div className="flex h-48 items-center justify-center">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : !chartData?.length ? null : (chartData?.length ?? 0) < 3 ? (
|
||||
) : !chartData?.length ? (
|
||||
// v1.4.43 W11-M6 — empty-window state.
|
||||
//
|
||||
// Pre-fix the chart silently `return null`ed when `chartData`
|
||||
// resolved empty (after-load, no points in the selected
|
||||
// range). Paired with the < 3-points hint, the user's
|
||||
// five visible chart states were:
|
||||
//
|
||||
// - skeleton (loading)
|
||||
// - 1-2 daily points / many raw → "Mehr Messtage erforderlich"
|
||||
// - 1-2 daily points / < 3 raw → "Erfasse mindestens 3 Einträge"
|
||||
// - ≥ 3 daily points → line chart
|
||||
// - 0 daily points → NOTHING
|
||||
//
|
||||
// The "nothing" branch read as a broken widget on the
|
||||
// dashboard. Paint a distinct empty state explaining the
|
||||
// selected range is empty so the user reaches for the range
|
||||
// tabs or the quick-add instead of suspecting a bug.
|
||||
<ChartEmptyState
|
||||
title={t("charts.noDataInRangeTitle")}
|
||||
description={t("charts.noDataInRangeDescription")}
|
||||
/>
|
||||
) : (chartData?.length ?? 0) < 3 ? (
|
||||
// v1.4.16 B1a — sparse-data placeholder. <3 daily points is too
|
||||
// few to render a meaningful trend; paint a friendly hint
|
||||
// instead so the dashboard doesn't look broken.
|
||||
|
||||
Reference in new issue
Block a user