mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 16:03:52 +00:00
fix(views): move stale-data-on-error fix to the shared query hooks
The previous Operator-only fix (checking rundown.revision) papered over a bug shared by every view loader: react-query sets status to 'error' on any failed fetch, including a background refetch, even when a prior successful fetch's data is still cached. All view loaders treated that as "no data" and blanked the whole view. Add deriveQueryStatus(), a small helper that only reports 'error' when a query has never received data, and apply it in the five base data hooks (useRundown, useRundownById, useCustomFields, useSettings, useProjectData, useViewSettings). This fixes the class of bug for every consumer (Operator, Timer, Backstage, Studio, Countdown, TimelinePage, ProjectInfo, RundownList, CuesheetTable) at the source, so the Operator-specific revision check can be reverted back to the plain status check.
This commit is contained in:
@@ -4,6 +4,7 @@ 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 = {};
|
||||
|
||||
@@ -15,5 +16,5 @@ export default function useCustomFields() {
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
||||
return { data: data ?? placeholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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, refetch } = useQuery({
|
||||
@@ -13,7 +14,7 @@ export default function useProjectData() {
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
return { data: data ?? projectDataPlaceholder, status, isFetching, isError, refetch };
|
||||
return { data: data ?? projectDataPlaceholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
export function useUpdateProjectData() {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
@@ -50,7 +51,7 @@ export default function useRundown() {
|
||||
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
||||
}, [loadedRundownId, queryClient]);
|
||||
|
||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||
return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching };
|
||||
}
|
||||
|
||||
export function useRundownWithMetadata() {
|
||||
@@ -135,5 +136,5 @@ export function useRundownById(rundownId: string | null | undefined) {
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
});
|
||||
|
||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching };
|
||||
return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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, refetch } = useQuery({
|
||||
@@ -22,5 +23,5 @@ export default function useSettings() {
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, refetch };
|
||||
return { data: data ?? ontimePlaceholderSettings, status: deriveQueryStatus(status, data), isFetching, isError, refetch };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 } = useQuery({
|
||||
@@ -24,5 +25,5 @@ export default function useViewSettings() {
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data ?? viewsSettingsPlaceholder, status, mutateAsync };
|
||||
return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, data), mutateAsync };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { deriveQueryStatus } from '../queryUtils';
|
||||
|
||||
test('keeps pending and success statuses unchanged', () => {
|
||||
expect(deriveQueryStatus('pending', undefined)).toBe('pending');
|
||||
expect(deriveQueryStatus('success', { some: 'data' })).toBe('success');
|
||||
});
|
||||
|
||||
test('keeps error status when there is no data', () => {
|
||||
expect(deriveQueryStatus('error', undefined)).toBe('error');
|
||||
});
|
||||
|
||||
test('downgrades error to success when data is still available', () => {
|
||||
expect(deriveQueryStatus('error', { some: 'data' })).toBe('success');
|
||||
expect(deriveQueryStatus('error', [])).toBe('success');
|
||||
expect(deriveQueryStatus('error', 0)).toBe('success');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { QueryStatus } from '@tanstack/react-query';
|
||||
|
||||
/**
|
||||
* A background refetch failure still leaves the last successfully fetched data in place.
|
||||
* In that case we want callers to keep treating the query as usable rather than erroring out,
|
||||
* so we only report 'error' when we have never received data for this query.
|
||||
*/
|
||||
export function deriveQueryStatus(status: QueryStatus, data: unknown): QueryStatus {
|
||||
if (status === 'error' && data !== undefined) {
|
||||
return 'success';
|
||||
}
|
||||
return status;
|
||||
}
|
||||
@@ -36,8 +36,7 @@ export default function OperatorLoader() {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
// only show the error state if we have never received data to fall back on
|
||||
if (status === 'error' && data.rundown.revision === -1) {
|
||||
if (status === 'error') {
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user