mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 08:39:34 +00:00
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.
This commit is contained in:
@@ -4,7 +4,6 @@ import { CustomFields } from 'ontime-types';
|
|||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { CUSTOM_FIELDS } from '../api/constants';
|
import { CUSTOM_FIELDS } from '../api/constants';
|
||||||
import { getCustomFields } from '../api/customFields';
|
import { getCustomFields } from '../api/customFields';
|
||||||
import { deriveQueryStatus } from '../utils/queryUtils';
|
|
||||||
|
|
||||||
const placeholder: CustomFields = {};
|
const placeholder: CustomFields = {};
|
||||||
|
|
||||||
@@ -16,5 +15,5 @@ export default function useCustomFields() {
|
|||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? placeholder, status: deriveQueryStatus(status, isLoadingError), isFetching, isError, refetch };
|
return { data: data ?? placeholder, status, isFetching, isError, isLoadingError, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|||||||
import { PROJECT_DATA } from '../api/constants';
|
import { PROJECT_DATA } from '../api/constants';
|
||||||
import { getProjectData, postProjectData } from '../api/project';
|
import { getProjectData, postProjectData } from '../api/project';
|
||||||
import { projectDataPlaceholder } from '../models/ProjectData';
|
import { projectDataPlaceholder } from '../models/ProjectData';
|
||||||
import { deriveQueryStatus } from '../utils/queryUtils';
|
|
||||||
|
|
||||||
export default function useProjectData() {
|
export default function useProjectData() {
|
||||||
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
||||||
@@ -14,13 +13,7 @@ export default function useProjectData() {
|
|||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { data: data ?? projectDataPlaceholder, status, isFetching, isError, isLoadingError, refetch };
|
||||||
data: data ?? projectDataPlaceholder,
|
|
||||||
status: deriveQueryStatus(status, isLoadingError),
|
|
||||||
isFetching,
|
|
||||||
isError,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateProjectData() {
|
export function useUpdateProjectData() {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
|||||||
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
|
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
|
||||||
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
|
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
import { deriveQueryStatus } from '../utils/queryUtils';
|
|
||||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
import { useProjectRundowns } from './useProjectRundowns';
|
import { useProjectRundowns } from './useProjectRundowns';
|
||||||
|
|
||||||
@@ -51,20 +50,14 @@ export default function useRundown() {
|
|||||||
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
||||||
}, [loadedRundownId, queryClient]);
|
}, [loadedRundownId, queryClient]);
|
||||||
|
|
||||||
return {
|
return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
|
||||||
data: data ?? cachedRundownPlaceholder,
|
|
||||||
status: deriveQueryStatus(status, isLoadingError),
|
|
||||||
isError,
|
|
||||||
refetch,
|
|
||||||
isFetching,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRundownWithMetadata() {
|
export function useRundownWithMetadata() {
|
||||||
const { data, status } = useRundown();
|
const { data, status, isLoadingError } = useRundown();
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
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
|
* built from the order and rundown fields
|
||||||
*/
|
*/
|
||||||
export function useFlatRundown() {
|
export function useFlatRundown() {
|
||||||
const { data, status } = useRundown();
|
const { data, status, isLoadingError } = useRundown();
|
||||||
|
|
||||||
const flatRundown = useMemo(() => {
|
const flatRundown = useMemo(() => {
|
||||||
if (data.revision === -1) {
|
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);
|
return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return { data: flatRundown, rundownId: data.id, status };
|
return { data: flatRundown, rundownId: data.id, status, isLoadingError };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFlatRundownWithMetadata() {
|
export function useFlatRundownWithMetadata() {
|
||||||
const { data, status } = useRundown();
|
const { data, status, isLoadingError } = useRundown();
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
|
|
||||||
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
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,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
|
||||||
data: data ?? cachedRundownPlaceholder,
|
|
||||||
status: deriveQueryStatus(status, isLoadingError),
|
|
||||||
isError,
|
|
||||||
refetch,
|
|
||||||
isFetching,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type RundownSource = {
|
|||||||
rundown: Rundown;
|
rundown: Rundown;
|
||||||
flatRundown: ExtendedEntry[];
|
flatRundown: ExtendedEntry[];
|
||||||
status: string;
|
status: string;
|
||||||
|
isLoadingError: boolean;
|
||||||
selectedEventId: EntryId | null;
|
selectedEventId: EntryId | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu
|
|||||||
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
|
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
|
||||||
const runtimeSelectedEventId = useSelectedEventId();
|
const runtimeSelectedEventId = useSelectedEventId();
|
||||||
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
|
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
|
||||||
const { data: rundown, status } = useRundownById(rundownId);
|
const { data: rundown, status, isLoadingError } = useRundownById(rundownId);
|
||||||
const flatRundown = useMemo(
|
const flatRundown = useMemo(
|
||||||
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
||||||
[effectiveSelectedEventId, rundown],
|
[effectiveSelectedEventId, rundown],
|
||||||
@@ -47,8 +48,9 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu
|
|||||||
rundown,
|
rundown,
|
||||||
flatRundown,
|
flatRundown,
|
||||||
status,
|
status,
|
||||||
|
isLoadingError,
|
||||||
selectedEventId: effectiveSelectedEventId,
|
selectedEventId: effectiveSelectedEventId,
|
||||||
}),
|
}),
|
||||||
[effectiveSelectedEventId, flatRundown, rundown, rundownId, status],
|
[effectiveSelectedEventId, flatRundown, isLoadingError, rundown, rundownId, status],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { unobfuscate } from 'ontime-utils';
|
|||||||
import { APP_SETTINGS } from '../api/constants';
|
import { APP_SETTINGS } from '../api/constants';
|
||||||
import { getSettings } from '../api/settings';
|
import { getSettings } from '../api/settings';
|
||||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||||
import { deriveQueryStatus } from '../utils/queryUtils';
|
|
||||||
|
|
||||||
export default function useSettings() {
|
export default function useSettings() {
|
||||||
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
||||||
@@ -23,11 +22,5 @@ export default function useSettings() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, isLoadingError, refetch };
|
||||||
data: data ?? ontimePlaceholderSettings,
|
|
||||||
status: deriveQueryStatus(status, isLoadingError),
|
|
||||||
isFetching,
|
|
||||||
isError,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { getViewSettings, postViewSettings } from '../../common/api/viewSettings
|
|||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { VIEW_SETTINGS } from '../api/constants';
|
import { VIEW_SETTINGS } from '../api/constants';
|
||||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||||
import { deriveQueryStatus } from '../utils/queryUtils';
|
|
||||||
|
|
||||||
export default function useViewSettings() {
|
export default function useViewSettings() {
|
||||||
const { data, status, isLoadingError } = useQuery({
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -14,9 +14,18 @@ export interface OperatorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useOperatorData(): ViewData<OperatorData> {
|
export function useOperatorData(): ViewData<OperatorData> {
|
||||||
const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata();
|
const {
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
data: rundown,
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
rundownMetadata,
|
||||||
|
status: rundownStatus,
|
||||||
|
isLoadingError: rundownIsLoadingError,
|
||||||
|
} = useRundownWithMetadata();
|
||||||
|
const {
|
||||||
|
data: customFields,
|
||||||
|
status: customFieldStatus,
|
||||||
|
isLoadingError: customFieldIsLoadingError,
|
||||||
|
} = useCustomFields();
|
||||||
|
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -25,6 +34,10 @@ export function useOperatorData(): ViewData<OperatorData> {
|
|||||||
customFields,
|
customFields,
|
||||||
settings,
|
settings,
|
||||||
},
|
},
|
||||||
status: aggregateQueryStatus([rundownStatus, customFieldStatus, settingsStatus]),
|
status: aggregateQueryStatus([
|
||||||
|
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
|
||||||
|
{ status: customFieldStatus, isLoadingError: customFieldIsLoadingError },
|
||||||
|
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Rundown from './Rundown';
|
|||||||
|
|
||||||
export default memo(RundownList);
|
export default memo(RundownList);
|
||||||
function RundownList() {
|
function RundownList() {
|
||||||
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
const { data, status, isLoadingError, rundownMetadata } = useRundownWithMetadata();
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ function RundownList() {
|
|||||||
return <EmptyFill text='Loading…' />;
|
return <EmptyFill text='Loading…' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
if (isLoadingError) {
|
||||||
return <EmptyFill text={getLocalizedString('common.no_data')} />;
|
return <EmptyFill text={getLocalizedString('common.no_data')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ export function useBackstageData(): ViewData<BackstageData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundown();
|
const { data: rundownData, status: rundownStatus, isLoadingError: rundownIsLoadingError } = useFlatRundown();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
const {
|
||||||
|
data: customFields,
|
||||||
|
status: customFieldsStatus,
|
||||||
|
isLoadingError: customFieldsIsLoadingError,
|
||||||
|
} = useCustomFields();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -33,6 +37,11 @@ export function useBackstageData(): ViewData<BackstageData> {
|
|||||||
isMirrored,
|
isMirrored,
|
||||||
settings,
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ export function useCountdownData(): ViewData<CountdownData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
const {
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
data: rundownData,
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
status: rundownStatus,
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -34,6 +42,11 @@ export function useCountdownData(): ViewData<CountdownData> {
|
|||||||
isMirrored,
|
isMirrored,
|
||||||
settings,
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function CuesheetTable({
|
|||||||
isCurrentRundown,
|
isCurrentRundown,
|
||||||
insertElement,
|
insertElement,
|
||||||
}: CuesheetTableProps) {
|
}: CuesheetTableProps) {
|
||||||
const { flatRundown, status, selectedEventId } = source;
|
const { flatRundown, status, isLoadingError, selectedEventId } = source;
|
||||||
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
||||||
@@ -235,7 +235,7 @@ export default function CuesheetTable({
|
|||||||
return <EmptyFill text='Loading…' className={style.tableLoading} />;
|
return <EmptyFill text='Loading…' className={style.tableLoading} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
if (isLoadingError) {
|
||||||
return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />;
|
return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ProjectData } from 'ontime-types';
|
|||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
import { ViewData } from '../utils/viewLoader.utils';
|
import { ViewData, aggregateQueryStatus } from '../utils/viewLoader.utils';
|
||||||
|
|
||||||
export interface ProjectInfoData {
|
export interface ProjectInfoData {
|
||||||
projectData: ProjectData;
|
projectData: ProjectData;
|
||||||
@@ -14,13 +14,13 @@ export function useProjectInfoData(): ViewData<ProjectInfoData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
projectData,
|
projectData,
|
||||||
isMirrored,
|
isMirrored,
|
||||||
},
|
},
|
||||||
status: projectDataStatus,
|
status: aggregateQueryStatus([{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError }]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,18 @@ export function useStudioData(): ViewData<StudioData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
|
||||||
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
const {
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
data: viewSettings,
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
status: viewSettingsStatus,
|
||||||
|
isLoadingError: viewSettingsIsLoadingError,
|
||||||
|
} = useViewSettings();
|
||||||
|
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
|
||||||
|
const {
|
||||||
|
data: customFields,
|
||||||
|
status: customFieldsStatus,
|
||||||
|
isLoadingError: customFieldsIsLoadingError,
|
||||||
|
} = useCustomFields();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -33,6 +41,11 @@ export function useStudioData(): ViewData<StudioData> {
|
|||||||
settings,
|
settings,
|
||||||
viewSettings,
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,18 @@ export interface TimelineData {
|
|||||||
|
|
||||||
export function useTimelineData(): ViewData<TimelineData> {
|
export function useTimelineData(): ViewData<TimelineData> {
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata();
|
const {
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
data: rundownData,
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
status: rundownStatus,
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -28,6 +36,11 @@ export function useTimelineData(): ViewData<TimelineData> {
|
|||||||
projectData,
|
projectData,
|
||||||
settings,
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,19 @@ export function useTimerData(): ViewData<TimerData> {
|
|||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
|
||||||
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings();
|
const {
|
||||||
const { data: settings, status: settingsStatus } = useSettings();
|
data: viewSettings,
|
||||||
const { data: customFields, status: customFieldsStatus } = useCustomFields();
|
status: viewSettingsStatus,
|
||||||
const { data: rundown, status: rundownStatus } = useRundown();
|
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;
|
const { entries } = rundown;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -39,11 +47,11 @@ export function useTimerData(): ViewData<TimerData> {
|
|||||||
entries,
|
entries,
|
||||||
},
|
},
|
||||||
status: aggregateQueryStatus([
|
status: aggregateQueryStatus([
|
||||||
projectDataStatus,
|
{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
|
||||||
viewSettingsStatus,
|
{ status: viewSettingsStatus, isLoadingError: viewSettingsIsLoadingError },
|
||||||
settingsStatus,
|
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
|
||||||
customFieldsStatus,
|
{ status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
|
||||||
rundownStatus,
|
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,27 @@ export type ViewData<T> = {
|
|||||||
status: QueryStatus;
|
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.
|
* Aggregates a loading status from multiple queries, for the purpose of deciding
|
||||||
* If all statuses are 'pending', returns 'pending'.
|
* whether a view can render.
|
||||||
* If all statuses are 'success', returns 'success'.
|
* - 'pending' while any query hasn't settled yet (no result, first fetch in flight)
|
||||||
* If any status is 'error', returns 'error'.
|
* - '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 {
|
export function aggregateQueryStatus(queries: AggregatableQuery[]): QueryStatus {
|
||||||
if (statuses.every((status) => status === 'pending')) {
|
const allSettled = queries.every((query) => query.status !== 'pending');
|
||||||
|
if (!allSettled) {
|
||||||
return 'pending';
|
return 'pending';
|
||||||
}
|
}
|
||||||
if (statuses.every((status) => status === 'success')) {
|
if (queries.some((query) => query.isLoadingError)) {
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
if (statuses.some((status) => status === 'error')) {
|
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
return 'pending';
|
return 'success';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user