Q186: data that can no longer be updated says how old it is and why
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f5526b075c
commit
6f6ed48549
5 files changed
+71
-3
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { StaleBanner } from "./StaleBanner";
|
||||
import { oneOf, readViewState, seedViewState, writeViewState } from "../shared/view-state";
|
||||
import { LeaveGuardContext } from "./leave-guard";
|
||||
import { t } from "../i18n/i18n";
|
||||
@@ -27,7 +28,7 @@ import { OfficePage } from "../pages/OfficePage";
|
||||
const isRoute = oneOf(ROUTE_IDS);
|
||||
|
||||
export function App() {
|
||||
const { snapshot, syncFailure, actions } = useSnapshot();
|
||||
const { snapshot, syncFailure, syncedAt, actions } = useSnapshot();
|
||||
const [routeChoice, setRoute] = useState<RouteId | null>(null);
|
||||
// A focus belongs to the company it was opened in; another company's page never receives it.
|
||||
const [focusChoice, setFocus] = useState<{ orgId: string | null; focus: Focus } | null>(null);
|
||||
@@ -129,6 +130,8 @@ export function App() {
|
||||
<ErrorBoundary key={pageKey} scope="page">{page}</ErrorBoundary>
|
||||
</Shell>
|
||||
</LeaveGuardContext.Provider>
|
||||
{/* A failed manual retry is already recorded in syncFailure, which the banner shows. */}
|
||||
{syncFailure && <StaleBanner failure={syncFailure} syncedAt={syncedAt} onRetry={() => void actions.reload().catch(() => undefined)} />}
|
||||
{leaving && (
|
||||
<div className="modal-backdrop" role="presentation">
|
||||
<div className="modal" role="alertdialog" aria-label={t("Leave this page?")}>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Button } from "../shared/ui";
|
||||
import { t } from "../i18n/i18n";
|
||||
import { formatTime } from "../pages/types";
|
||||
import type { SyncFailure } from "./useSnapshot";
|
||||
|
||||
/** Says the data on screen is no longer current: from when it is, why updating failed, and that it keeps retrying. */
|
||||
export function StaleBanner({ failure, syncedAt, onRetry }: { failure: SyncFailure; syncedAt: string | null; onRetry: () => void }) {
|
||||
return (
|
||||
<div role="alert" className="card" style={{ position: "fixed", top: 8, left: "50%", transform: "translateX(-50%)", zIndex: 900, display: "flex", gap: 10, alignItems: "center", padding: "8px 12px", borderColor: "var(--amber, var(--red))", maxWidth: "min(720px, calc(100vw - 32px))" }}>
|
||||
<div style={{ fontSize: 13, minWidth: 0 }}>
|
||||
<strong>{t("Not up to date.")}</strong>{" "}
|
||||
{syncedAt ? t("Showing data from {time}.", { time: formatTime(syncedAt) }) : null}{" "}
|
||||
<span style={{ color: "var(--text-2)", wordBreak: "break-word" }}>{t("Updating failed: {reason} (attempt {count}).", { reason: failure.message, count: failure.attempts })}</span>
|
||||
</div>
|
||||
<Button size="small" onClick={onRetry}>{t("Retry now")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,10 +16,13 @@ export interface SyncFailure {
|
||||
export function useSnapshot(): {
|
||||
snapshot: AppSnapshot | null;
|
||||
syncFailure: SyncFailure | null;
|
||||
/** When the data on screen was last read in full (ISO), so stale data can say how old it is. */
|
||||
syncedAt: string | null;
|
||||
actions: SnapshotActions;
|
||||
} {
|
||||
const [snapshot, setSnapshot] = useState<AppSnapshot | null>(null);
|
||||
const [syncFailure, setSyncFailure] = useState<SyncFailure | null>(null);
|
||||
const [syncedAt, setSyncedAt] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@@ -27,6 +30,7 @@ export function useSnapshot(): {
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
setSnapshot(result.data);
|
||||
setSyncFailure(null);
|
||||
setSyncedAt(new Date().toISOString());
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setSyncFailure((current) => ({ message, attempts: (current?.attempts ?? 0) + 1 }));
|
||||
@@ -39,7 +43,11 @@ export function useSnapshot(): {
|
||||
): Promise<T | null> => {
|
||||
const result = await operation();
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
if (result.data) setSnapshot(result.data as AppSnapshot);
|
||||
if (result.data) {
|
||||
setSnapshot(result.data as AppSnapshot);
|
||||
setSyncFailure(null);
|
||||
setSyncedAt(new Date().toISOString());
|
||||
}
|
||||
return result.data;
|
||||
}, []);
|
||||
|
||||
@@ -87,5 +95,5 @@ export function useSnapshot(): {
|
||||
}, [load]);
|
||||
|
||||
const actions = useMemo<SnapshotActions>(() => ({ run, reload: load }), [run, load]);
|
||||
return { snapshot, syncFailure, actions };
|
||||
return { snapshot, syncFailure, syncedAt, actions };
|
||||
}
|
||||
@@ -696,6 +696,9 @@ export const zh: Record<string, string> = {
|
||||
"Working language": "工作语言",
|
||||
"That did not work": "操作未成功",
|
||||
"Working…": "处理中…",
|
||||
"Not up to date.": "数据不是最新的。",
|
||||
"Showing data from {time}.": "当前显示 {time} 的数据。",
|
||||
"Updating failed: {reason} (attempt {count}).": "更新失败:{reason}(第 {count} 次尝试)。",
|
||||
"Discard your changes?": "放弃你的修改?",
|
||||
"Keep editing": "继续编辑",
|
||||
"Discard": "放弃",
|
||||
|
||||
Reference in new issue
Block a user