mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
refactor(views): consistent loading and empty states
This commit is contained in:
@@ -35,7 +35,7 @@ export default function BackstageLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Backstage {...data} />;
|
||||
|
||||
@@ -7,7 +7,7 @@ $dot-spacing: 1.5rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--background-color-override, $viewer-background-color);
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
@@ -21,26 +21,26 @@ $dot-spacing: 1.5rem;
|
||||
height: $dot-size;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-color-override, $ontime-color);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:nth-child(1) {
|
||||
left: $dot-size;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
animation: lds-ellipsis1 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
left: $dot-size;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
animation: lds-ellipsis2 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
left: calc($dot-size + $dot-spacing);
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
animation: lds-ellipsis2 1s infinite;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
left: calc($dot-size + 2 * $dot-spacing);
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
animation: lds-ellipsis3 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,21 +80,7 @@ $item-height: 3.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function CountdownLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Countdown {...data} />;
|
||||
@@ -87,7 +87,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
||||
|
||||
{!hasEvents && (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('common.no_data')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('common.no_data')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -121,7 +121,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (subscriptions.length === 0) {
|
||||
return (
|
||||
<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}>
|
||||
<IoAdd /> Add
|
||||
</Button>
|
||||
@@ -137,7 +137,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (subscribedEvents.length === 0) {
|
||||
return (
|
||||
<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}>
|
||||
<IoAdd /> Add
|
||||
</Button>
|
||||
@@ -154,7 +154,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (eventsToShow.length === 0) {
|
||||
return (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('countdown.all_have_finished')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('countdown.all_have_finished')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { MaybeString, ProjectRundown } from 'ontime-types';
|
||||
import { memo, use, useMemo } from 'react';
|
||||
|
||||
import Select from '../../common/components/select/Select';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import { PresetContext } from '../../common/context/PresetContext';
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
|
||||
@@ -34,40 +33,34 @@ function CuesheetTableWrapper({
|
||||
const preset = use(PresetContext);
|
||||
const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId;
|
||||
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown });
|
||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||
const { data: customFields } = useCustomFields();
|
||||
|
||||
const columns = useMemo(
|
||||
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
|
||||
[customFields, cuesheetMode, preset],
|
||||
);
|
||||
|
||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||
|
||||
return (
|
||||
<CuesheetDnd columns={columns}>
|
||||
{isLoading ? (
|
||||
<EmptyPage text='Loading...' />
|
||||
) : (
|
||||
<CuesheetTable
|
||||
columns={columns}
|
||||
source={source}
|
||||
cuesheetMode={cuesheetMode}
|
||||
tableRoot='cuesheet'
|
||||
setCuesheetMode={setCuesheetMode}
|
||||
isCurrentRundown={isCurrentRundown}
|
||||
insertElement={
|
||||
<>
|
||||
<RundownSelect
|
||||
cuesheetMode={cuesheetMode}
|
||||
selectedRundownId={selectedRundownId}
|
||||
loadedRundownId={loadedRundownId}
|
||||
setSelectedRundownId={setSelectedRundownId}
|
||||
projectRundowns={projectRundowns}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CuesheetTable
|
||||
columns={columns}
|
||||
source={source}
|
||||
cuesheetMode={cuesheetMode}
|
||||
tableRoot='cuesheet'
|
||||
setCuesheetMode={setCuesheetMode}
|
||||
isCurrentRundown={isCurrentRundown}
|
||||
insertElement={
|
||||
<>
|
||||
<RundownSelect
|
||||
cuesheetMode={cuesheetMode}
|
||||
selectedRundownId={selectedRundownId}
|
||||
loadedRundownId={loadedRundownId}
|
||||
setSelectedRundownId={setSelectedRundownId}
|
||||
projectRundowns={projectRundowns}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</CuesheetDnd>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ $table-header-font-size: calc(1rem - 2px);
|
||||
|
||||
@include rows.cuesheet-row-columns($table-header-font-size);
|
||||
|
||||
.tableLoading {
|
||||
grid-area: table;
|
||||
}
|
||||
|
||||
.cuesheet {
|
||||
font-size: $table-font-size;
|
||||
font-weight: 400;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
TableVirtuosoHandle,
|
||||
} from 'react-virtuoso';
|
||||
|
||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||
import EmptyFill from '../../../common/components/state/EmptyFill';
|
||||
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||
@@ -19,6 +19,7 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||
import { AppMode } from '../../../ontimeConfig';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||
import { useCuesheetPermissions } from '../useTablePermissions';
|
||||
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
||||
@@ -64,6 +65,7 @@ export default function CuesheetTable({
|
||||
}: CuesheetTableProps) {
|
||||
const { flatRundown, status, selectedEventId } = source;
|
||||
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
||||
|
||||
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
||||
@@ -228,10 +230,13 @@ export default function CuesheetTable({
|
||||
});
|
||||
}, [cuesheetMode, hideIndexColumn, table]);
|
||||
|
||||
const isLoading = !flatRundown || status === 'pending';
|
||||
// avoid showing the editable empty state before we know whether the rundown is actually empty
|
||||
if (status === 'pending') {
|
||||
return <EmptyFill text='Loading…' className={style.tableLoading} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <EmptyPage text='Loading...' />;
|
||||
if (status === 'error') {
|
||||
return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -100,13 +100,17 @@ $content-width: min(100%, 1100px);
|
||||
}
|
||||
|
||||
.info__image-container {
|
||||
flex: 0 0 min(128px, 25%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0 0 min(192px, 25%);
|
||||
}
|
||||
|
||||
.info__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.link.info__value {
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function ProjectInfoLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <ProjectInfo {...data} />;
|
||||
@@ -73,12 +73,11 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
||||
</div>
|
||||
)}
|
||||
{projectData.custom.map((info, idx) => {
|
||||
const hasImage = Boolean(info.url);
|
||||
return (
|
||||
// oxlint-disable-next-line react/no-array-index-key - we only have the index to go of here
|
||||
<div key={`${info.title}-${idx}`} className='info__card'>
|
||||
{info.title && <div className='info__label'>{info.title}</div>}
|
||||
{hasImage ? (
|
||||
{info.url ? (
|
||||
<div className='info__media'>
|
||||
<InfoImage src={info.url} />
|
||||
{info.value && <div className='info__value'>{info.value}</div>}
|
||||
@@ -108,10 +107,6 @@ function InfoCard({ label, children }: InfoCardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an user provided image, collapsing itself if the image fails to load
|
||||
* We remove the container along with the image to avoid leaving an empty gap in the card
|
||||
*/
|
||||
function InfoImage({ src }: { src: string }) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function StudioLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Studio {...data} />;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OntimeView } from 'ontime-types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import EmptyFill from '../../common/components/state/EmptyFill';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -29,7 +30,7 @@ export default function TimelinePageLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <TimelinePage {...data} />;
|
||||
@@ -73,7 +74,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
|
||||
totalDuration={totalDuration}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPage text={getLocalizedString('common.no_data')} />
|
||||
<EmptyFill text={getLocalizedString('common.no_data')} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function TimerLoader() {
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
return <EmptyPage variant='error' text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Timer {...data} />;
|
||||
|
||||
Reference in New Issue
Block a user