From a393fd2fb14cd811416fd74256eedddd97bd3829 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Wed, 22 Jul 2026 04:14:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20R3=20S1=20review=20fixes=20=E2=80=94?= =?UTF-8?q?=20don't=20blank=20data=20on=20transient=20refetch=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The honest-state branch now guards on `isError && !data`, so a background/window-focus refetch failure keeps already-visible content instead of replacing the list with a full error screen (regression the bare `isError` introduced). Applied to AuditLog, Channels, ChannelDiscovery, PlexBrowse, PlexPlaylistView, Messages(people), Feed — matching the guard already used in Messages(conversations)/NotificationsPanel/Scheduler. - Drop the unused DataTable loading/error/onRetry props (every table page uses the outer StateMessage branch, not the props) — reverts DataTable to its simple empty block. - StateMessage announces errors with role=alert/aria-live=assertive (was polite for all tones). - Remove the now-unused common.empty string (en+hu), left over from the removed QueryState render-prop. --- frontend/src/components/AuditLog.tsx | 2 +- frontend/src/components/ChannelDiscovery.tsx | 2 +- frontend/src/components/Channels.tsx | 2 +- frontend/src/components/DataTable.tsx | 20 ++------------------ frontend/src/components/Feed.tsx | 2 +- frontend/src/components/Messages.tsx | 2 +- frontend/src/components/PlexBrowse.tsx | 2 +- frontend/src/components/PlexPlaylistView.tsx | 2 +- frontend/src/components/QueryState.tsx | 14 ++++++++------ frontend/src/i18n/locales/en/common.json | 3 +-- frontend/src/i18n/locales/hu/common.json | 3 +-- 11 files changed, 19 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx index 106ccff1..c4feda96 100644 --- a/frontend/src/components/AuditLog.tsx +++ b/frontend/src/components/AuditLog.tsx @@ -54,7 +54,7 @@ export default function AuditLog() {

{t("audit.intro")}

- {q.isError ? ( + {q.isError && !q.data ? ( q.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 74c58945..124368bd 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -224,7 +224,7 @@ export default function ChannelDiscovery({
- {query.isError ? ( + {query.isError && !query.data ? ( query.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 439f8dd0..760d6c1b 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -710,7 +710,7 @@ export default function Channels() { (status chips + pager) rides in the fixed band right above it (controlsInBand). The card layout swaps the table for a virtualized grid over the same rows (its pager and status chips ride in the fixed band above instead). */} - {channelsQuery.isError ? ( + {channelsQuery.isError && !channelsQuery.data ? ( channelsQuery.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index 9620dd4f..a1750074 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -4,7 +4,6 @@ import { sortRows, cycleSort, type SortState } from "../lib/columnSort"; import Pager, { hasPagerContent } from "./Pager"; import { PageToolbar } from "./PageShell"; import { ColumnHead, alignClass, filterRows, type Column, type FilterMap } from "./tableHeader"; -import { StateMessage } from "./QueryState"; // A reusable, client-side data table: per-column sort + filter (in the header) + pagination, // with its sort/filter/page state optionally persisted to localStorage so a reload (F5) keeps @@ -45,9 +44,6 @@ export default function DataTable({ pageSizeOptions = [10, 25, 50, 100], persistKey, emptyText, - loading = false, - error = false, - onRetry, rowClassName, controlsPosition = "bottom", controlsLeading, @@ -65,12 +61,6 @@ export default function DataTable({ pageSizeOptions?: number[]; persistKey?: string; emptyText?: string; - // Honest states (R3): when the caller's query is loading or errored, pass these so the table - // shows a loading / error(+retry) message instead of an "empty" list that lies about a failed - // fetch. `error` suppresses the empty-text; `onRetry` wires the retry button. - loading?: boolean; - error?: boolean; - onRetry?: () => void; rowClassName?: (row: T) => string; controlsPosition?: "top" | "bottom" | "both"; controlsLeading?: ReactNode; @@ -256,17 +246,11 @@ export default function DataTable({ ))}
- {error ? ( - - {t("common.loadError")} - - ) : loading && paged.length === 0 ? ( - {t("common.loading")} - ) : paged.length === 0 ? ( + {paged.length === 0 && (
{emptyText ?? t("datatable.empty")}
- ) : null} + )} {controlsPosition === "bottom" && controls} {controlsPosition === "both" && bottomControls} diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index 695e9686..22c02484 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -494,7 +494,7 @@ export default function Feed({ } if (query.isLoading) return
{t("feed.loading")}
; - if (query.isError) + if (query.isError && !query.data) return ( query.refetch()}> {t("feed.loadError")} diff --git a/frontend/src/components/Messages.tsx b/frontend/src/components/Messages.tsx index 44d7b4b2..b3ab6ff8 100644 --- a/frontend/src/components/Messages.tsx +++ b/frontend/src/components/Messages.tsx @@ -225,7 +225,7 @@ function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBac placeholder={t("messages.searchPeople")} className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent" /> - {q.isError ? ( + {q.isError && !q.data ? ( q.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 5c7c272e..ecd4b32c 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -358,7 +358,7 @@ export default function PlexBrowse() { )} - {browseQ.isError ? ( + {browseQ.isError && !browseQ.data ? ( browseQ.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index e02ba77b..0e847bd4 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -577,7 +577,7 @@ export default function PlexPlaylistView({ - {q.isError ? ( + {q.isError && !q.data ? ( q.refetch()}> {t("common.loadError")} diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx index 4278bb23..57f42199 100644 --- a/frontend/src/components/QueryState.tsx +++ b/frontend/src/components/QueryState.tsx @@ -22,8 +22,9 @@ export function StateMessage({ return (
{tone === "error" && } @@ -42,7 +43,8 @@ export function StateMessage({ ); } -// The honest-state convention every page now follows: check `isError` BEFORE the empty branch so a -// failed fetch renders this message (with Retry) instead of the page's "empty" text — the R3 bug -// where a backend restart read as "No channels". `StateMessage` is the shared building block; -// pages wire it into their existing loading/empty ternary (and DataTable takes loading/error props). +// The honest-state convention every page now follows: check `isError && ` BEFORE the empty +// branch, so a failed fetch renders this message (with Retry) instead of the page's "empty" text +// — the R3 bug where a backend restart read as "No channels". The `&& ` guard matters: a +// transient refetch error while data is already on screen keeps the data rather than blanking it to +// an error. `StateMessage` is the shared building block pages wire into their loading/empty ternary. diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 178b878b..deed2f72 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -19,6 +19,5 @@ "demoSharedNotice": "You're in the shared demo account. Everything you do here — playlists, hidden videos, settings — is communal and may be changed by other demo visitors at the same time.", "backToTop": "Back to top", "retry": "Retry", - "loadError": "Couldn't load this — check your connection and try again.", - "empty": "Nothing here yet." + "loadError": "Couldn't load this — check your connection and try again." } diff --git a/frontend/src/i18n/locales/hu/common.json b/frontend/src/i18n/locales/hu/common.json index bb2c83a6..a3386834 100644 --- a/frontend/src/i18n/locales/hu/common.json +++ b/frontend/src/i18n/locales/hu/common.json @@ -19,6 +19,5 @@ "demoSharedNotice": "Egy közös demo fiókban vagy. Minden, amit itt csinálsz — lejátszási listák, elrejtett videók, beállítások — közös, és más demo látogatók egyszerre módosíthatják.", "backToTop": "Vissza a tetejére", "retry": "Újra", - "loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra.", - "empty": "Itt még nincs semmi." + "loadError": "Nem sikerült betölteni — ellenőrizd a kapcsolatot, és próbáld újra." }