mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 00:29:41 +00:00
Unify and refine viewer empty/loading/error states
Every viewer page (Timer, Countdown, Backstage, Studio, Timeline, ProjectInfo, Operator) re-implemented the same data-loader flow by hand, each with a hard-coded, unlocalized error string and drifting empty-state styling. This centralizes that flow and elevates the shared presentation. - Add ViewDataBoundary: a single component that renders the shared loading, error and optional no-data states from a QueryStatus, so every view handles them the same obvious way. Refactor all seven view loaders through it. - Localize the error state via new common.fetch_error / common.fetch_error_hint keys (en + de/es/fr/it/pt), replacing the duplicated English literal with a two-line "Something went wrong / Please refresh the page" message. - Elevate the shared Empty component: legible viewer-secondary color (was 10% white), responsive title sizing, softened illustration, optional supporting subtitle line, and a reduced-motion-safe fade-in. - Unify loading: Cuesheet and the rundown table now use the animated Loader like every other view instead of an untranslated "Loading..." page. - Tidy up: EmptyTableBody uses Empty's text prop, drop Countdown's now-redundant size override, and remove a stray semicolon rendered in the ProjectInfo empty state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vq5XLTSQhU9ckZmCSW2SEr
This commit is contained in:
@@ -1,20 +1,49 @@
|
|||||||
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.emptyContainer {
|
.emptyContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $white-10;
|
color: $viewer-secondary-color;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation: empty-fade-in $viewer-transition-time ease both;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(100%, 24rem);
|
width: min(100%, 16rem);
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
opacity: 0.8;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
display: block;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
|
margin-top: min(2vh, 16px);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 2em;
|
font-size: $title-font-size;
|
||||||
|
max-width: min(100%, 600px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
display: block;
|
||||||
|
margin-inline: auto;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: $base-font-size;
|
||||||
|
color: $viewer-label-color;
|
||||||
max-width: min(100%, 600px);
|
max-width: min(100%, 600px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes empty-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ import style from './Empty.module.scss';
|
|||||||
|
|
||||||
interface EmptyProps {
|
interface EmptyProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
secondary?: string;
|
||||||
injectedStyles?: CSSProperties;
|
injectedStyles?: CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Empty({ text, className, injectedStyles }: EmptyProps) {
|
export default function Empty({ text, secondary, className, injectedStyles }: EmptyProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.emptyContainer, className])} style={injectedStyles}>
|
<div className={cx([style.emptyContainer, className])} style={injectedStyles}>
|
||||||
<EmptyImage className={style.empty} />
|
<EmptyImage className={style.empty} />
|
||||||
{text && <span className={style.text}>{text}</span>}
|
{text && <span className={style.text}>{text}</span>}
|
||||||
|
{secondary && <span className={style.secondary}>{secondary}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import style from './EmptyPage.module.scss';
|
|||||||
|
|
||||||
interface EmptyPageProps {
|
interface EmptyPageProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
secondary?: string;
|
||||||
injectedStyles?: CSSProperties;
|
injectedStyles?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmptyPage({ text, injectedStyles }: EmptyPageProps) {
|
export default function EmptyPage({ text, secondary, injectedStyles }: EmptyPageProps) {
|
||||||
return (
|
return (
|
||||||
<div className={style.page}>
|
<div className={style.page}>
|
||||||
<Empty text={text} injectedStyles={injectedStyles} />
|
<Empty text={text} secondary={secondary} injectedStyles={injectedStyles} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,4 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) {
|
|||||||
<tbody className={style.emptyContainer}>
|
<tbody className={style.emptyContainer}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={99} className={style.emptyCell}>
|
<td colSpan={99} className={style.emptyCell}>
|
||||||
<Empty injectedStyles={{ marginTop: '5vh' }} />
|
<Empty text={text} injectedStyles={{ marginTop: '5vh' }} />
|
||||||
<span className={style.text}>{text}</span>
|
|
||||||
{handleAddNew && (
|
{handleAddNew && (
|
||||||
<div className={style.inline}>
|
<div className={style.inline}>
|
||||||
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
|
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OntimeView, isOntimeEvent, isOntimeGroup } from 'ontime-types';
|
import { OntimeView, isOntimeEvent, isOntimeGroup } from 'ontime-types';
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useSelectedEventId } from '../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../common/hooks/useSocket';
|
||||||
@@ -10,7 +9,7 @@ import { cx } from '../../common/utils/styleUtils';
|
|||||||
import { throttle } from '../../common/utils/throttle';
|
import { throttle } from '../../common/utils/throttle';
|
||||||
import { getDefaultFormat } from '../../common/utils/time';
|
import { getDefaultFormat } from '../../common/utils/time';
|
||||||
import { isTouchDevice } from '../../externals';
|
import { isTouchDevice } from '../../externals';
|
||||||
import Loader from '../../views/common/loader/Loader';
|
import ViewDataBoundary from '../../views/common/view-data-boundary/ViewDataBoundary';
|
||||||
import CustomFieldEditModal from './custom-field-edit-modal/CustomFieldEditModal';
|
import CustomFieldEditModal from './custom-field-edit-modal/CustomFieldEditModal';
|
||||||
import FollowButton from './follow-button/FollowButton';
|
import FollowButton from './follow-button/FollowButton';
|
||||||
import OperatorEvent from './operator-event/OperatorEvent';
|
import OperatorEvent from './operator-event/OperatorEvent';
|
||||||
@@ -30,15 +29,11 @@ export default function OperatorLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Operator');
|
useWindowTitle('Operator');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<Operator {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Operator {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
|
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { memo, useEffect, useMemo } from 'react';
|
import { memo, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
|
||||||
import { EntryActionsProvider } from '../../../common/context/EntryActionsContext';
|
import { EntryActionsProvider } from '../../../common/context/EntryActionsContext';
|
||||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||||
import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
|
import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||||
|
import Loader from '../../../views/common/loader/Loader';
|
||||||
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
||||||
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
||||||
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
|
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
|
||||||
@@ -32,13 +32,15 @@ function RundownTable() {
|
|||||||
|
|
||||||
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
||||||
|
|
||||||
|
// TODO: adopt the shared ViewDataBoundary (views/common/view-data-boundary) once this
|
||||||
|
// table exposes a single query status instead of the ad-hoc isLoading check
|
||||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntryActionsProvider actions={actions}>
|
<EntryActionsProvider actions={actions}>
|
||||||
<CuesheetDnd columns={columns} tableRoot='editor'>
|
<CuesheetDnd columns={columns} tableRoot='editor'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<EmptyPage text='Loading...' />
|
<Loader />
|
||||||
) : (
|
) : (
|
||||||
<CuesheetTable columns={columns} source={source} cuesheetMode={editorMode} tableRoot='editor' />
|
<CuesheetTable columns={columns} source={source} cuesheetMode={editorMode} tableRoot='editor' />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const langDe: TranslationObject = {
|
|||||||
'common.started_at': 'Gestartet am',
|
'common.started_at': 'Gestartet am',
|
||||||
'common.time_now': 'Aktuelle Zeit',
|
'common.time_now': 'Aktuelle Zeit',
|
||||||
'common.no_data': 'Keine Daten',
|
'common.no_data': 'Keine Daten',
|
||||||
|
'common.fetch_error': 'Etwas ist schiefgelaufen',
|
||||||
|
'common.fetch_error_hint': 'Bitte aktualisieren Sie die Seite',
|
||||||
'countdown.ended': 'Veranstaltung endete um',
|
'countdown.ended': 'Veranstaltung endete um',
|
||||||
'countdown.running': 'Veranstaltung läuft',
|
'countdown.running': 'Veranstaltung läuft',
|
||||||
'countdown.group_running': 'Veranstaltung in Gruppe läuft',
|
'countdown.group_running': 'Veranstaltung in Gruppe läuft',
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const langEs: TranslationObject = {
|
|||||||
'common.started_at': 'Iniciado en',
|
'common.started_at': 'Iniciado en',
|
||||||
'common.time_now': 'Ahora',
|
'common.time_now': 'Ahora',
|
||||||
'common.no_data': 'Sin datos',
|
'common.no_data': 'Sin datos',
|
||||||
|
'common.fetch_error': 'Algo salió mal',
|
||||||
|
'common.fetch_error_hint': 'Actualiza la página',
|
||||||
'countdown.ended': 'Evento finalizado a las',
|
'countdown.ended': 'Evento finalizado a las',
|
||||||
'countdown.running': 'Evento en curso',
|
'countdown.running': 'Evento en curso',
|
||||||
'countdown.group_running': 'Evento en grupo en curso',
|
'countdown.group_running': 'Evento en grupo en curso',
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const langFr: TranslationObject = {
|
|||||||
'common.started_at': 'Commencé à',
|
'common.started_at': 'Commencé à',
|
||||||
'common.time_now': 'Heure',
|
'common.time_now': 'Heure',
|
||||||
'common.no_data': 'Aucune donnée',
|
'common.no_data': 'Aucune donnée',
|
||||||
|
'common.fetch_error': 'Une erreur est survenue',
|
||||||
|
'common.fetch_error_hint': 'Veuillez actualiser la page',
|
||||||
'countdown.ended': 'Évènement terminé à',
|
'countdown.ended': 'Évènement terminé à',
|
||||||
'countdown.running': 'Évènement en cours',
|
'countdown.running': 'Évènement en cours',
|
||||||
'countdown.group_running': 'Évènement du groupe en cours',
|
'countdown.group_running': 'Évènement du groupe en cours',
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const langIt: TranslationObject = {
|
|||||||
'common.started_at': 'Iniziato Alle',
|
'common.started_at': 'Iniziato Alle',
|
||||||
'common.time_now': 'Ora attuale',
|
'common.time_now': 'Ora attuale',
|
||||||
'common.no_data': 'Nessun dato disponibile',
|
'common.no_data': 'Nessun dato disponibile',
|
||||||
|
'common.fetch_error': 'Qualcosa è andato storto',
|
||||||
|
'common.fetch_error_hint': 'Aggiorna la pagina',
|
||||||
'countdown.ended': 'Evento finito alle',
|
'countdown.ended': 'Evento finito alle',
|
||||||
'countdown.running': 'Evento in corso',
|
'countdown.running': 'Evento in corso',
|
||||||
'countdown.group_running': 'Evento nel gruppo in corso',
|
'countdown.group_running': 'Evento nel gruppo in corso',
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const langPt: TranslationObject = {
|
|||||||
'common.started_at': 'Iniciado em',
|
'common.started_at': 'Iniciado em',
|
||||||
'common.time_now': 'Hora atual',
|
'common.time_now': 'Hora atual',
|
||||||
'common.no_data': 'Sem dados',
|
'common.no_data': 'Sem dados',
|
||||||
|
'common.fetch_error': 'Algo correu mal',
|
||||||
|
'common.fetch_error_hint': 'Atualize a página',
|
||||||
'countdown.ended': 'Evento encerrado às',
|
'countdown.ended': 'Evento encerrado às',
|
||||||
'countdown.running': 'Evento em andamento',
|
'countdown.running': 'Evento em andamento',
|
||||||
'countdown.group_running': 'Evento em grupo em andamento',
|
'countdown.group_running': 'Evento em grupo em andamento',
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import ProgressBar from '../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../common/components/progress-bar/ProgressBar';
|
||||||
import QRCode from '../../common/components/qr-code/QrCode';
|
import QRCode from '../../common/components/qr-code/QrCode';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import TitleCard from '../../common/components/title-card/TitleCard';
|
import TitleCard from '../../common/components/title-card/TitleCard';
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
@@ -16,9 +15,9 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|||||||
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
|
||||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
||||||
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
||||||
import { BackstageData, useBackstageData } from './useBackstageData';
|
import { BackstageData, useBackstageData } from './useBackstageData';
|
||||||
@@ -30,15 +29,11 @@ export default function BackstageLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Backstage');
|
useWindowTitle('Backstage');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<Backstage {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Backstage {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
|
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { QueryStatus } from '@tanstack/react-query';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
import Loader from '../loader/Loader';
|
||||||
|
|
||||||
|
interface ViewDataBoundaryProps {
|
||||||
|
status: QueryStatus;
|
||||||
|
/** render a full-page "no data" state instead of the children */
|
||||||
|
isEmpty?: boolean;
|
||||||
|
/** overrides the default "no data" message when isEmpty is true */
|
||||||
|
emptyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard gate for a viewer's data-loading flow.
|
||||||
|
* Renders the shared loading, error and (optional) empty states so every view
|
||||||
|
* handles them the same way. Children are only rendered once the data is ready.
|
||||||
|
*/
|
||||||
|
export default function ViewDataBoundary({
|
||||||
|
status,
|
||||||
|
isEmpty,
|
||||||
|
emptyText,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<ViewDataBoundaryProps>) {
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
|
return (
|
||||||
|
<EmptyPage
|
||||||
|
text={getLocalizedString('common.fetch_error')}
|
||||||
|
secondary={getLocalizedString('common.fetch_error_hint')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
return <EmptyPage text={emptyText ?? getLocalizedString('common.no_data')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -86,17 +86,6 @@ $item-height: 3.5rem;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state__content {
|
|
||||||
max-width: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
max-width: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-container {
|
.list-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { IoAdd } from 'react-icons/io5';
|
|||||||
|
|
||||||
import Button from '../../common/components/buttons/Button';
|
import Button from '../../common/components/buttons/Button';
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
|
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
|
||||||
@@ -22,8 +21,8 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|||||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
||||||
import { getOrderedSubscriptions, resolveSubscriptionTarget } from './countdown.utils';
|
import { getOrderedSubscriptions, resolveSubscriptionTarget } from './countdown.utils';
|
||||||
import CountdownSelect from './CountdownSelect';
|
import CountdownSelect from './CountdownSelect';
|
||||||
@@ -38,15 +37,11 @@ export default function CountdownLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Countdown');
|
useWindowTitle('Countdown');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<Countdown {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Countdown {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Countdown({ customFields, rundownData, projectData, isMirrored, settings }: CountdownData) {
|
function Countdown({ customFields, rundownData, projectData, isMirrored, settings }: CountdownData) {
|
||||||
@@ -87,7 +82,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
|||||||
|
|
||||||
{!hasEvents && (
|
{!hasEvents && (
|
||||||
<div className='empty-state'>
|
<div className='empty-state'>
|
||||||
<Empty text={getLocalizedString('common.no_data')} className='empty-state__content' />
|
<Empty text={getLocalizedString('common.no_data')} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -121,7 +116,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
|||||||
if (subscriptions.length === 0) {
|
if (subscriptions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className='empty-state'>
|
<div className='empty-state'>
|
||||||
<Empty text={getLocalizedString('countdown.select_event')} className='empty-state__content' />
|
<Empty text={getLocalizedString('countdown.select_event')} />
|
||||||
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
||||||
<IoAdd /> Add
|
<IoAdd /> Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -137,7 +132,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
|||||||
if (subscribedEvents.length === 0) {
|
if (subscribedEvents.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className='empty-state'>
|
<div className='empty-state'>
|
||||||
<Empty text={getLocalizedString('countdown.select_event')} className='empty-state__content' />
|
<Empty text={getLocalizedString('countdown.select_event')} />
|
||||||
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
||||||
<IoAdd /> Add
|
<IoAdd /> Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -154,7 +149,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
|||||||
if (eventsToShow.length === 0) {
|
if (eventsToShow.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className='empty-state'>
|
<div className='empty-state'>
|
||||||
<Empty text={getLocalizedString('countdown.all_have_finished')} className='empty-state__content' />
|
<Empty text={getLocalizedString('countdown.all_have_finished')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { MaybeString, ProjectRundown } from 'ontime-types';
|
|||||||
import { memo, use, useMemo } from 'react';
|
import { memo, use, useMemo } from 'react';
|
||||||
|
|
||||||
import Select from '../../common/components/select/Select';
|
import Select from '../../common/components/select/Select';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
|
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
|
||||||
import { AppMode } from '../../ontimeConfig';
|
import { AppMode } from '../../ontimeConfig';
|
||||||
|
import Loader from '../common/loader/Loader';
|
||||||
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
||||||
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
||||||
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||||
@@ -41,12 +41,14 @@ function CuesheetTableWrapper({
|
|||||||
[customFields, cuesheetMode, preset],
|
[customFields, cuesheetMode, preset],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: adopt the shared ViewDataBoundary (views/common/view-data-boundary) once this
|
||||||
|
// table exposes a single query status instead of the ad-hoc isLoading check
|
||||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CuesheetDnd columns={columns}>
|
<CuesheetDnd columns={columns}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<EmptyPage text='Loading...' />
|
<Loader />
|
||||||
) : (
|
) : (
|
||||||
<CuesheetTable
|
<CuesheetTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
TableVirtuosoHandle,
|
TableVirtuosoHandle,
|
||||||
} from 'react-virtuoso';
|
} from 'react-virtuoso';
|
||||||
|
|
||||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
|
||||||
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||||
@@ -19,6 +18,7 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
|||||||
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
||||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
|
import Loader from '../../common/loader/Loader';
|
||||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||||
import { useCuesheetPermissions } from '../useTablePermissions';
|
import { useCuesheetPermissions } from '../useTablePermissions';
|
||||||
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
||||||
@@ -231,7 +231,7 @@ export default function CuesheetTable({
|
|||||||
const isLoading = !flatRundown || status === 'pending';
|
const isLoading = !flatRundown || status === 'pending';
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <EmptyPage text='Loading...' />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
|||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import { ProjectInfoData, useProjectInfoData } from './useProjectInfoData';
|
import { ProjectInfoData, useProjectInfoData } from './useProjectInfoData';
|
||||||
|
|
||||||
import './ProjectInfo.scss';
|
import './ProjectInfo.scss';
|
||||||
@@ -16,15 +16,11 @@ export default function ProjectInfoLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Project info');
|
useWindowTitle('Project info');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<ProjectInfo {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ProjectInfo {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
||||||
@@ -41,7 +37,7 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||||
<EmptyPage text={getLocalizedString('common.no_data')} />;
|
<EmptyPage text={getLocalizedString('common.no_data')} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { OntimeView } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { getDefaultFormat } from '../../common/utils/time';
|
import { getDefaultFormat } from '../../common/utils/time';
|
||||||
import Loader from '../common/loader/Loader';
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import { getStudioOptions, useStudioOptions } from './studio.options';
|
import { getStudioOptions, useStudioOptions } from './studio.options';
|
||||||
import StudioClock from './StudioClock';
|
import StudioClock from './StudioClock';
|
||||||
import StudioTimers from './StudioTimers';
|
import StudioTimers from './StudioTimers';
|
||||||
@@ -20,15 +19,11 @@ export default function StudioLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Studio Clock');
|
useWindowTitle('Studio Clock');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<Studio {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Studio {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Studio({ customFields, projectData, isMirrored, settings, viewSettings }: StudioData) {
|
function Studio({ customFields, projectData, isMirrored, settings, viewSettings }: StudioData) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { useSelectedEventId } from '../../common/hooks/useSocket';
|
|||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import Timeline from './Timeline';
|
import Timeline from './Timeline';
|
||||||
import { getTimelineOptions, useTimelineOptions } from './timeline.options';
|
import { getTimelineOptions, useTimelineOptions } from './timeline.options';
|
||||||
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
||||||
@@ -24,15 +24,11 @@ export default function TimelinePageLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Timeline');
|
useWindowTitle('Timeline');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<TimelinePage {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <TimelinePage {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
|
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { FitText } from '../../common/components/fit-text/FitText';
|
import { FitText } from '../../common/components/fit-text/FitText';
|
||||||
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
|
||||||
import TitleCard from '../../common/components/title-card/TitleCard';
|
import TitleCard from '../../common/components/title-card/TitleCard';
|
||||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
@@ -13,8 +12,8 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../common/loader/Loader';
|
|
||||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||||
import { getFormattedTimer, getTimerByType } from '../common/viewUtils';
|
import { getFormattedTimer, getTimerByType } from '../common/viewUtils';
|
||||||
import { getTimerColour } from '../utils/presentation.utils';
|
import { getTimerColour } from '../utils/presentation.utils';
|
||||||
import { getTimerOptions, useTimerOptions } from './timer.options';
|
import { getTimerOptions, useTimerOptions } from './timer.options';
|
||||||
@@ -38,15 +37,11 @@ export default function TimerLoader() {
|
|||||||
|
|
||||||
useWindowTitle('Timer');
|
useWindowTitle('Timer');
|
||||||
|
|
||||||
if (status === 'pending') {
|
return (
|
||||||
return <Loader />;
|
<ViewDataBoundary status={status}>
|
||||||
}
|
<Timer {...data} />
|
||||||
|
</ViewDataBoundary>
|
||||||
if (status === 'error') {
|
);
|
||||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Timer {...data} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Timer({ customFields, projectData, isMirrored, settings, viewSettings, entries }: TimerData) {
|
function Timer({ customFields, projectData, isMirrored, settings, viewSettings, entries }: TimerData) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export const langEn = {
|
|||||||
'common.started_at': 'Started At',
|
'common.started_at': 'Started At',
|
||||||
'common.time_now': 'Time now',
|
'common.time_now': 'Time now',
|
||||||
'common.no_data': 'No data',
|
'common.no_data': 'No data',
|
||||||
|
'common.fetch_error': 'Something went wrong',
|
||||||
|
'common.fetch_error_hint': 'Please refresh the page',
|
||||||
'countdown.ended': 'Event ended at',
|
'countdown.ended': 'Event ended at',
|
||||||
'countdown.running': 'Event running',
|
'countdown.running': 'Event running',
|
||||||
'countdown.group_running': 'Event in group running',
|
'countdown.group_running': 'Event in group running',
|
||||||
|
|||||||
Reference in New Issue
Block a user