Task e57facc2: Undo and live SSE updates (#210)
This commit was merged in pull request #210.
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ApiForbiddenError } from "../api/client";
|
||||
import type { TransitionEvent } from "../api/events";
|
||||
import { useTransitions } from "../api/useTransitions";
|
||||
import { exportHistoryCsv, fetchHistory, type HistoryFilter, type HistoryRow } from "../api/history";
|
||||
import { localTodayIso } from "../schedule/cycle";
|
||||
import { dayLabel, groupByDay, rowMeta, rowTime, shiftIso } from "./format";
|
||||
@@ -51,7 +53,8 @@ interface Loaded {
|
||||
type LoadState =
|
||||
| { kind: "loading" }
|
||||
| { kind: "error"; message: string }
|
||||
| { kind: "ready"; data: Loaded };
|
||||
/** `key` is the (filter, start, end) the rows were loaded for. */
|
||||
| { kind: "ready"; data: Loaded; key: string };
|
||||
|
||||
/**
|
||||
* The selected filter's rows plus the period stats. Stats always come from
|
||||
@@ -166,6 +169,14 @@ export default function HistoryTab({ today = localTodayIso() }: { today?: string
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const exportingRef = useRef(false);
|
||||
const start = shiftIso(today, -(RANGE_DAYS - 1));
|
||||
const key = `${filter}|${start}|${today}`;
|
||||
|
||||
// A quest transition is a new history row (or changes a derived missed row);
|
||||
// a day-complete adds none. The refetch keeps the active filter and range.
|
||||
useTransitions((event: TransitionEvent) => {
|
||||
if (event.type === "nestquest_child_day_complete") return;
|
||||
setAttempt((n) => n + 1);
|
||||
});
|
||||
|
||||
/** Download the CSV for the active filter over the displayed range. */
|
||||
async function exportCsv() {
|
||||
@@ -190,10 +201,13 @@ export default function HistoryTab({ today = localTodayIso() }: { today?: string
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setState({ kind: "loading" });
|
||||
// Rows already loaded for this (filter, range) stay on screen while they
|
||||
// refresh; any other rows give way to the loading state.
|
||||
const isCurrent = (prev: LoadState) => prev.kind === "ready" && prev.key === key;
|
||||
setState((prev) => (isCurrent(prev) ? prev : { kind: "loading" }));
|
||||
loadHistory(filter, start, today)
|
||||
.then((data) => {
|
||||
if (!cancelled) setState({ kind: "ready", data });
|
||||
if (!cancelled) setState({ kind: "ready", data, key });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (cancelled) return;
|
||||
@@ -201,31 +215,36 @@ export default function HistoryTab({ today = localTodayIso() }: { today?: string
|
||||
error instanceof ApiForbiddenError
|
||||
? error.message
|
||||
: "The history could not be loaded. Check your connection and try again.";
|
||||
setState({ kind: "error", message });
|
||||
// A failed refresh keeps the rows already on screen.
|
||||
setState((prev) => (isCurrent(prev) ? prev : { kind: "error", message }));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [filter, start, today, attempt]);
|
||||
}, [filter, start, today, key, attempt]);
|
||||
|
||||
// Never render one filter's rows under another's chip, even for the render
|
||||
// before the load effect runs.
|
||||
const shown: LoadState = state.kind === "ready" && state.key !== key ? { kind: "loading" } : state;
|
||||
|
||||
let body;
|
||||
if (state.kind === "loading") {
|
||||
if (shown.kind === "loading") {
|
||||
body = (
|
||||
<div className="history-status" role="status" data-testid="history-loading">
|
||||
Loading history…
|
||||
</div>
|
||||
);
|
||||
} else if (state.kind === "error") {
|
||||
} else if (shown.kind === "error") {
|
||||
body = (
|
||||
<div className="history-status history-status--error" role="alert" data-testid="history-error">
|
||||
<p>{state.message}</p>
|
||||
<p>{shown.message}</p>
|
||||
<button type="button" className="history-retry" onClick={() => setAttempt((n) => n + 1)}>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const groups = groupByDay(state.data.rows);
|
||||
const groups = groupByDay(shown.data.rows);
|
||||
body =
|
||||
groups.length > 0 ? (
|
||||
groups.map((group) => (
|
||||
@@ -259,7 +278,7 @@ export default function HistoryTab({ today = localTodayIso() }: { today?: string
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{state.kind === "ready" ? <StatRow stats={state.data.stats} /> : null}
|
||||
{shown.kind === "ready" ? <StatRow stats={shown.data.stats} /> : null}
|
||||
<div className="history-chips" role="group" aria-label="Filter events">
|
||||
{FILTERS.map((item) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user