From f2455fec177c785f4a77ef597d283b4b02bcc546 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 05:28:49 +0000 Subject: [PATCH] refactor(views): keep query status truthful, push render logic to the view boundary Reworks the previous fix, which had base hooks (useRundown, useSettings, useCustomFields, useProjectData, useViewSettings) rewrite their own status/isError to hide background-refetch failures. That was misleading: those hooks are consumed well beyond the 9 view loaders, and other callers may legitimately want to know a fetch genuinely failed. Instead the base hooks now return react-query's status/isError untouched, plus the isLoadingError flag react-query already computes (true only when a query has never received data). aggregateQueryStatus (viewLoader.utils.ts) takes {status, isLoadingError} pairs and only reports 'error' once every query has settled and at least one never got data; a background/refetch error with data present now aggregates to 'success'. The two single-query consumers outside that aggregator (RundownList/CuesheetTable via useScopedRundown, and ProjectInfo) branch on isLoadingError directly instead of status === 'error'. Removes the standalone deriveQueryStatus helper introduced in the prior iteration, since the logic now lives in the one place that already existed for this purpose (aggregateQueryStatus) rather than a new abstraction. --- .../src/common/hooks-query/useCustomFields.ts | 3 +- .../src/common/hooks-query/useProjectData.ts | 9 +----- .../src/common/hooks-query/useRundown.ts | 29 +++++-------------- .../common/hooks-query/useScopedRundown.ts | 6 ++-- .../src/common/hooks-query/useSettings.ts | 9 +----- .../src/common/hooks-query/useViewSettings.ts | 3 +- .../common/utils/__tests__/queryUtils.test.ts | 14 --------- apps/client/src/common/utils/queryUtils.ts | 14 --------- .../src/features/operator/useOperatorData.ts | 21 +++++++++++--- .../src/features/rundown/RundownList.tsx | 4 +-- .../src/views/backstage/useBackstageData.ts | 19 ++++++++---- .../src/views/countdown/useCountdownData.ts | 23 +++++++++++---- .../cuesheet/cuesheet-table/CuesheetTable.tsx | 4 +-- .../views/project-info/useProjectInfoData.ts | 6 ++-- apps/client/src/views/studio/useStudioData.ts | 23 +++++++++++---- .../src/views/timeline/useTimelineData.ts | 23 +++++++++++---- apps/client/src/views/timer/useTimerData.ts | 28 +++++++++++------- .../src/views/utils/viewLoader.utils.ts | 28 +++++++++++------- 18 files changed, 143 insertions(+), 123 deletions(-) delete mode 100644 apps/client/src/common/utils/__tests__/queryUtils.test.ts delete mode 100644 apps/client/src/common/utils/queryUtils.ts diff --git a/apps/client/src/common/hooks-query/useCustomFields.ts b/apps/client/src/common/hooks-query/useCustomFields.ts index 92c679a70..3908f2d87 100644 --- a/apps/client/src/common/hooks-query/useCustomFields.ts +++ b/apps/client/src/common/hooks-query/useCustomFields.ts @@ -4,7 +4,6 @@ import { CustomFields } from 'ontime-types'; import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { CUSTOM_FIELDS } from '../api/constants'; import { getCustomFields } from '../api/customFields'; -import { deriveQueryStatus } from '../utils/queryUtils'; const placeholder: CustomFields = {}; @@ -16,5 +15,5 @@ export default function useCustomFields() { refetchInterval: queryRefetchIntervalSlow, }); - return { data: data ?? placeholder, status: deriveQueryStatus(status, isLoadingError), isFetching, isError, refetch }; + return { data: data ?? placeholder, status, isFetching, isError, isLoadingError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useProjectData.ts b/apps/client/src/common/hooks-query/useProjectData.ts index ea7425e78..e867175b0 100644 --- a/apps/client/src/common/hooks-query/useProjectData.ts +++ b/apps/client/src/common/hooks-query/useProjectData.ts @@ -4,7 +4,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { PROJECT_DATA } from '../api/constants'; import { getProjectData, postProjectData } from '../api/project'; import { projectDataPlaceholder } from '../models/ProjectData'; -import { deriveQueryStatus } from '../utils/queryUtils'; export default function useProjectData() { const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ @@ -14,13 +13,7 @@ export default function useProjectData() { refetchInterval: queryRefetchIntervalSlow, }); - return { - data: data ?? projectDataPlaceholder, - status: deriveQueryStatus(status, isLoadingError), - isFetching, - isError, - refetch, - }; + return { data: data ?? projectDataPlaceholder, status, isFetching, isError, isLoadingError, refetch }; } export function useUpdateProjectData() { diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index 1b0ea42dd..85037b733 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -6,7 +6,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants'; import { fetchCurrentRundown, fetchRundown } from '../api/rundown'; import { useSelectedEventId } from '../hooks/useSocket'; -import { deriveQueryStatus } from '../utils/queryUtils'; import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata'; import { useProjectRundowns } from './useProjectRundowns'; @@ -51,20 +50,14 @@ export default function useRundown() { queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); }, [loadedRundownId, queryClient]); - return { - data: data ?? cachedRundownPlaceholder, - status: deriveQueryStatus(status, isLoadingError), - isError, - refetch, - isFetching, - }; + return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching }; } export function useRundownWithMetadata() { - const { data, status } = useRundown(); + const { data, status, isLoadingError } = useRundown(); const selectedEventId = useSelectedEventId(); const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]); - return { data, status, rundownMetadata }; + return { data, status, isLoadingError, rundownMetadata }; } /** @@ -72,7 +65,7 @@ export function useRundownWithMetadata() { * built from the order and rundown fields */ export function useFlatRundown() { - const { data, status } = useRundown(); + const { data, status, isLoadingError } = useRundown(); const flatRundown = useMemo(() => { if (data.revision === -1) { @@ -81,15 +74,15 @@ export function useFlatRundown() { return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined); }, [data]); - return { data: flatRundown, rundownId: data.id, status }; + return { data: flatRundown, rundownId: data.id, status, isLoadingError }; } export function useFlatRundownWithMetadata() { - const { data, status } = useRundown(); + const { data, status, isLoadingError } = useRundown(); const selectedEventId = useSelectedEventId(); const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]); - return { data: rundownWithMetadata, status }; + return { data: rundownWithMetadata, status, isLoadingError }; } /** @@ -142,11 +135,5 @@ export function useRundownById(rundownId: string | null | undefined) { refetchInterval: queryRefetchIntervalSlow, }); - return { - data: data ?? cachedRundownPlaceholder, - status: deriveQueryStatus(status, isLoadingError), - isError, - refetch, - isFetching, - }; + return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching }; } diff --git a/apps/client/src/common/hooks-query/useScopedRundown.ts b/apps/client/src/common/hooks-query/useScopedRundown.ts index a14707d7f..68cb76385 100644 --- a/apps/client/src/common/hooks-query/useScopedRundown.ts +++ b/apps/client/src/common/hooks-query/useScopedRundown.ts @@ -11,6 +11,7 @@ export type RundownSource = { rundown: Rundown; flatRundown: ExtendedEntry[]; status: string; + isLoadingError: boolean; selectedEventId: EntryId | null; }; @@ -35,7 +36,7 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId; const runtimeSelectedEventId = useSelectedEventId(); const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null; - const { data: rundown, status } = useRundownById(rundownId); + const { data: rundown, status, isLoadingError } = useRundownById(rundownId); const flatRundown = useMemo( () => getFlatRundownMetadata(rundown, effectiveSelectedEventId), [effectiveSelectedEventId, rundown], @@ -47,8 +48,9 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu rundown, flatRundown, status, + isLoadingError, selectedEventId: effectiveSelectedEventId, }), - [effectiveSelectedEventId, flatRundown, rundown, rundownId, status], + [effectiveSelectedEventId, flatRundown, isLoadingError, rundown, rundownId, status], ); } diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts index d0c6815ac..496e53a55 100644 --- a/apps/client/src/common/hooks-query/useSettings.ts +++ b/apps/client/src/common/hooks-query/useSettings.ts @@ -4,7 +4,6 @@ import { unobfuscate } from 'ontime-utils'; import { APP_SETTINGS } from '../api/constants'; import { getSettings } from '../api/settings'; import { ontimePlaceholderSettings } from '../models/OntimeSettings'; -import { deriveQueryStatus } from '../utils/queryUtils'; export default function useSettings() { const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ @@ -23,11 +22,5 @@ export default function useSettings() { }, }); - return { - data: data ?? ontimePlaceholderSettings, - status: deriveQueryStatus(status, isLoadingError), - isFetching, - isError, - refetch, - }; + return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, isLoadingError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useViewSettings.ts b/apps/client/src/common/hooks-query/useViewSettings.ts index c901a2bec..7c3671941 100644 --- a/apps/client/src/common/hooks-query/useViewSettings.ts +++ b/apps/client/src/common/hooks-query/useViewSettings.ts @@ -5,7 +5,6 @@ import { getViewSettings, postViewSettings } from '../../common/api/viewSettings import { ontimeQueryClient } from '../../common/queryClient'; import { VIEW_SETTINGS } from '../api/constants'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; -import { deriveQueryStatus } from '../utils/queryUtils'; export default function useViewSettings() { const { data, status, isLoadingError } = useQuery({ @@ -25,5 +24,5 @@ export default function useViewSettings() { }, }); - return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, isLoadingError), mutateAsync }; + return { data: data ?? viewsSettingsPlaceholder, status, isLoadingError, mutateAsync }; } diff --git a/apps/client/src/common/utils/__tests__/queryUtils.test.ts b/apps/client/src/common/utils/__tests__/queryUtils.test.ts deleted file mode 100644 index 22a43b286..000000000 --- a/apps/client/src/common/utils/__tests__/queryUtils.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { deriveQueryStatus } from '../queryUtils'; - -test('keeps pending and success statuses unchanged', () => { - expect(deriveQueryStatus('pending', false)).toBe('pending'); - expect(deriveQueryStatus('success', false)).toBe('success'); -}); - -test('keeps error status on a genuine loading error (no data ever received)', () => { - expect(deriveQueryStatus('error', true)).toBe('error'); -}); - -test('downgrades error to success on a refetch error (data still available)', () => { - expect(deriveQueryStatus('error', false)).toBe('success'); -}); diff --git a/apps/client/src/common/utils/queryUtils.ts b/apps/client/src/common/utils/queryUtils.ts deleted file mode 100644 index 813f40470..000000000 --- a/apps/client/src/common/utils/queryUtils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { QueryStatus } from '@tanstack/react-query'; - -/** - * A background refetch failure (react-query's isRefetchError) still leaves the last - * successfully fetched data in place, so we want callers to keep treating the query as - * usable rather than erroring out. Only a genuine isLoadingError (never received data) - * should surface as 'error'. - */ -export function deriveQueryStatus(status: QueryStatus, isLoadingError: boolean): QueryStatus { - if (status === 'error' && !isLoadingError) { - return 'success'; - } - return status; -} diff --git a/apps/client/src/features/operator/useOperatorData.ts b/apps/client/src/features/operator/useOperatorData.ts index 8eeeae6b2..73aad60e5 100644 --- a/apps/client/src/features/operator/useOperatorData.ts +++ b/apps/client/src/features/operator/useOperatorData.ts @@ -14,9 +14,18 @@ export interface OperatorData { } export function useOperatorData(): ViewData { - const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata(); - const { data: customFields, status: customFieldStatus } = useCustomFields(); - const { data: settings, status: settingsStatus } = useSettings(); + const { + data: rundown, + rundownMetadata, + status: rundownStatus, + isLoadingError: rundownIsLoadingError, + } = useRundownWithMetadata(); + const { + data: customFields, + status: customFieldStatus, + isLoadingError: customFieldIsLoadingError, + } = useCustomFields(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); return { data: { @@ -25,6 +34,10 @@ export function useOperatorData(): ViewData { customFields, settings, }, - status: aggregateQueryStatus([rundownStatus, customFieldStatus, settingsStatus]), + status: aggregateQueryStatus([ + { status: rundownStatus, isLoadingError: rundownIsLoadingError }, + { status: customFieldStatus, isLoadingError: customFieldIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + ]), }; } diff --git a/apps/client/src/features/rundown/RundownList.tsx b/apps/client/src/features/rundown/RundownList.tsx index 86d80906b..b69619556 100644 --- a/apps/client/src/features/rundown/RundownList.tsx +++ b/apps/client/src/features/rundown/RundownList.tsx @@ -8,7 +8,7 @@ import Rundown from './Rundown'; export default memo(RundownList); function RundownList() { - const { data, status, rundownMetadata } = useRundownWithMetadata(); + const { data, status, isLoadingError, rundownMetadata } = useRundownWithMetadata(); const featureData = useRundownEditor(); const { getLocalizedString } = useTranslation(); @@ -17,7 +17,7 @@ function RundownList() { return ; } - if (status === 'error') { + if (isLoadingError) { return ; } diff --git a/apps/client/src/views/backstage/useBackstageData.ts b/apps/client/src/views/backstage/useBackstageData.ts index 1a804edac..198201fe2 100644 --- a/apps/client/src/views/backstage/useBackstageData.ts +++ b/apps/client/src/views/backstage/useBackstageData.ts @@ -20,10 +20,14 @@ export function useBackstageData(): ViewData { const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data - const { data: rundownData, status: rundownStatus } = useFlatRundown(); - const { data: projectData, status: projectDataStatus } = useProjectData(); - const { data: settings, status: settingsStatus } = useSettings(); - const { data: customFields, status: customFieldsStatus } = useCustomFields(); + const { data: rundownData, status: rundownStatus, isLoadingError: rundownIsLoadingError } = useFlatRundown(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); + const { + data: customFields, + status: customFieldsStatus, + isLoadingError: customFieldsIsLoadingError, + } = useCustomFields(); return { data: { @@ -33,6 +37,11 @@ export function useBackstageData(): ViewData { isMirrored, settings, }, - status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), + status: aggregateQueryStatus([ + { status: rundownStatus, isLoadingError: rundownIsLoadingError }, + { status: projectDataStatus, isLoadingError: projectDataIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError }, + ]), }; } diff --git a/apps/client/src/views/countdown/useCountdownData.ts b/apps/client/src/views/countdown/useCountdownData.ts index 9faaf6c82..4c501b8fb 100644 --- a/apps/client/src/views/countdown/useCountdownData.ts +++ b/apps/client/src/views/countdown/useCountdownData.ts @@ -21,10 +21,18 @@ export function useCountdownData(): ViewData { const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data - const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(); - const { data: projectData, status: projectDataStatus } = useProjectData(); - const { data: settings, status: settingsStatus } = useSettings(); - const { data: customFields, status: customFieldsStatus } = useCustomFields(); + const { + data: rundownData, + status: rundownStatus, + isLoadingError: rundownIsLoadingError, + } = useFlatRundownWithMetadata(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); + const { + data: customFields, + status: customFieldsStatus, + isLoadingError: customFieldsIsLoadingError, + } = useCustomFields(); return { data: { @@ -34,6 +42,11 @@ export function useCountdownData(): ViewData { isMirrored, settings, }, - status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), + status: aggregateQueryStatus([ + { status: rundownStatus, isLoadingError: rundownIsLoadingError }, + { status: projectDataStatus, isLoadingError: projectDataIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError }, + ]), }; } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index 075f0b898..bba33370a 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -63,7 +63,7 @@ export default function CuesheetTable({ isCurrentRundown, insertElement, }: CuesheetTableProps) { - const { flatRundown, status, selectedEventId } = source; + const { flatRundown, status, isLoadingError, selectedEventId } = source; const { updateEntry, updateTimer, addEntry } = useEntryActionsContext(); const { getLocalizedString } = useTranslation(); const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit; @@ -235,7 +235,7 @@ export default function CuesheetTable({ return ; } - if (status === 'error') { + if (isLoadingError) { return ; } diff --git a/apps/client/src/views/project-info/useProjectInfoData.ts b/apps/client/src/views/project-info/useProjectInfoData.ts index 74596324c..50b949f39 100644 --- a/apps/client/src/views/project-info/useProjectInfoData.ts +++ b/apps/client/src/views/project-info/useProjectInfoData.ts @@ -2,7 +2,7 @@ import { ProjectData } from 'ontime-types'; import useProjectData from '../../common/hooks-query/useProjectData'; import { useViewOptionsStore } from '../../common/stores/viewOptions'; -import { ViewData } from '../utils/viewLoader.utils'; +import { ViewData, aggregateQueryStatus } from '../utils/viewLoader.utils'; export interface ProjectInfoData { projectData: ProjectData; @@ -14,13 +14,13 @@ export function useProjectInfoData(): ViewData { const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data - const { data: projectData, status: projectDataStatus } = useProjectData(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); return { data: { projectData, isMirrored, }, - status: projectDataStatus, + status: aggregateQueryStatus([{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError }]), }; } diff --git a/apps/client/src/views/studio/useStudioData.ts b/apps/client/src/views/studio/useStudioData.ts index d4867b844..2a9e2ce20 100644 --- a/apps/client/src/views/studio/useStudioData.ts +++ b/apps/client/src/views/studio/useStudioData.ts @@ -20,10 +20,18 @@ export function useStudioData(): ViewData { const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data - const { data: projectData, status: projectDataStatus } = useProjectData(); - const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); - const { data: settings, status: settingsStatus } = useSettings(); - const { data: customFields, status: customFieldsStatus } = useCustomFields(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); + const { + data: viewSettings, + status: viewSettingsStatus, + isLoadingError: viewSettingsIsLoadingError, + } = useViewSettings(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); + const { + data: customFields, + status: customFieldsStatus, + isLoadingError: customFieldsIsLoadingError, + } = useCustomFields(); return { data: { @@ -33,6 +41,11 @@ export function useStudioData(): ViewData { settings, viewSettings, }, - status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]), + status: aggregateQueryStatus([ + { status: projectDataStatus, isLoadingError: projectDataIsLoadingError }, + { status: viewSettingsStatus, isLoadingError: viewSettingsIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError }, + ]), }; } diff --git a/apps/client/src/views/timeline/useTimelineData.ts b/apps/client/src/views/timeline/useTimelineData.ts index bd2f996f6..283436a49 100644 --- a/apps/client/src/views/timeline/useTimelineData.ts +++ b/apps/client/src/views/timeline/useTimelineData.ts @@ -16,10 +16,18 @@ export interface TimelineData { export function useTimelineData(): ViewData { // HTTP API data - const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(); - const { data: projectData, status: projectDataStatus } = useProjectData(); - const { data: settings, status: settingsStatus } = useSettings(); - const { data: customFields, status: customFieldsStatus } = useCustomFields(); + const { + data: rundownData, + status: rundownStatus, + isLoadingError: rundownIsLoadingError, + } = useFlatRundownWithMetadata(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); + const { + data: customFields, + status: customFieldsStatus, + isLoadingError: customFieldsIsLoadingError, + } = useCustomFields(); return { data: { @@ -28,6 +36,11 @@ export function useTimelineData(): ViewData { projectData, settings, }, - status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), + status: aggregateQueryStatus([ + { status: rundownStatus, isLoadingError: rundownIsLoadingError }, + { status: projectDataStatus, isLoadingError: projectDataIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError }, + ]), }; } diff --git a/apps/client/src/views/timer/useTimerData.ts b/apps/client/src/views/timer/useTimerData.ts index 406912e0b..caf2f7e75 100644 --- a/apps/client/src/views/timer/useTimerData.ts +++ b/apps/client/src/views/timer/useTimerData.ts @@ -22,11 +22,19 @@ export function useTimerData(): ViewData { const isMirrored = useViewOptionsStore((state) => state.mirror); // HTTP API data - const { data: projectData, status: projectDataStatus } = useProjectData(); - const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); - const { data: settings, status: settingsStatus } = useSettings(); - const { data: customFields, status: customFieldsStatus } = useCustomFields(); - const { data: rundown, status: rundownStatus } = useRundown(); + const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData(); + const { + data: viewSettings, + status: viewSettingsStatus, + isLoadingError: viewSettingsIsLoadingError, + } = useViewSettings(); + const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings(); + const { + data: customFields, + status: customFieldsStatus, + isLoadingError: customFieldsIsLoadingError, + } = useCustomFields(); + const { data: rundown, status: rundownStatus, isLoadingError: rundownIsLoadingError } = useRundown(); const { entries } = rundown; return { @@ -39,11 +47,11 @@ export function useTimerData(): ViewData { entries, }, status: aggregateQueryStatus([ - projectDataStatus, - viewSettingsStatus, - settingsStatus, - customFieldsStatus, - rundownStatus, + { status: projectDataStatus, isLoadingError: projectDataIsLoadingError }, + { status: viewSettingsStatus, isLoadingError: viewSettingsIsLoadingError }, + { status: settingsStatus, isLoadingError: settingsIsLoadingError }, + { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError }, + { status: rundownStatus, isLoadingError: rundownIsLoadingError }, ]), }; } diff --git a/apps/client/src/views/utils/viewLoader.utils.ts b/apps/client/src/views/utils/viewLoader.utils.ts index 4aae1d1ce..48137b5ce 100644 --- a/apps/client/src/views/utils/viewLoader.utils.ts +++ b/apps/client/src/views/utils/viewLoader.utils.ts @@ -5,21 +5,27 @@ export type ViewData = { status: QueryStatus; }; +type AggregatableQuery = { + status: QueryStatus; + /** true only when the query has never received data, ie useQuery's isLoadingError */ + isLoadingError: boolean; +}; + /** - * Aggregates a loading status from multiple query statuses. - * If all statuses are 'pending', returns 'pending'. - * If all statuses are 'success', returns 'success'. - * If any status is 'error', returns 'error'. + * Aggregates a loading status from multiple queries, for the purpose of deciding + * whether a view can render. + * - 'pending' while any query hasn't settled yet (no result, first fetch in flight) + * - 'error' once all queries have settled, if any of them never received data + * - 'success' once all queries have settled and every one has data to show, + * even if a query's last (background) fetch failed */ -export function aggregateQueryStatus(statuses: QueryStatus[]): QueryStatus { - if (statuses.every((status) => status === 'pending')) { +export function aggregateQueryStatus(queries: AggregatableQuery[]): QueryStatus { + const allSettled = queries.every((query) => query.status !== 'pending'); + if (!allSettled) { return 'pending'; } - if (statuses.every((status) => status === 'success')) { - return 'success'; - } - if (statuses.some((status) => status === 'error')) { + if (queries.some((query) => query.isLoadingError)) { return 'error'; } - return 'pending'; + return 'success'; }