mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 14:39:06 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2455fec17 | |||
| f7b58f5ebd | |||
| 9e2dfaab16 | |||
| 65e1d3cfe9 | |||
| 94d54529ee | |||
| f7535651f6 |
@@ -1,20 +1,43 @@
|
|||||||
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
.emptyContainer {
|
.emptyContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $white-10;
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(100%, 24rem);
|
width: min(100%, 14rem);
|
||||||
margin-inline: auto;
|
margin: 0 auto -1.5rem;
|
||||||
opacity: 0.8;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
display: block;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
font-weight: 600;
|
font-weight: 400;
|
||||||
font-size: 2em;
|
font-size: clamp(1rem, 1.55vw, 1.5rem);
|
||||||
max-width: min(100%, 600px);
|
line-height: 1.35;
|
||||||
|
max-width: min(100%, 40rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: $error-red;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
opacity: 0.35;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIcon {
|
||||||
|
display: block;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin: -0.125rem auto 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
|
import { IoWarningOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
import EmptyImage from '../../../assets/images/empty.svg?react';
|
import EmptyImage from '../../../assets/images/empty.svg?react';
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
@@ -9,12 +10,18 @@ interface EmptyProps {
|
|||||||
text?: string;
|
text?: string;
|
||||||
injectedStyles?: CSSProperties;
|
injectedStyles?: CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Empty({ text, className, injectedStyles }: EmptyProps) {
|
export default function Empty({ text, className, injectedStyles, variant }: EmptyProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cx([style.emptyContainer, className])} style={injectedStyles}>
|
<div
|
||||||
|
className={cx([style.emptyContainer, variant === 'error' && style.error, className])}
|
||||||
|
style={injectedStyles}
|
||||||
|
role={variant === 'error' ? 'alert' : undefined}
|
||||||
|
>
|
||||||
<EmptyImage className={style.empty} />
|
<EmptyImage className={style.empty} />
|
||||||
|
{variant === 'error' && <IoWarningOutline className={style.errorIcon} aria-hidden />}
|
||||||
{text && <span className={style.text}>{text}</span>}
|
{text && <span className={style.text}>{text}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.fill {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-items: start center;
|
||||||
|
padding: clamp(4rem, 20dvh, 12rem) 1.5rem 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
import Empty from './Empty';
|
||||||
|
|
||||||
|
import style from './EmptyFill.module.scss';
|
||||||
|
|
||||||
|
interface EmptyFillProps {
|
||||||
|
text?: string;
|
||||||
|
/** placed on the fill wrapper — e.g. to assign a grid-area in a grid parent */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Container-filling empty/loading state for panels and grid/flex cells. */
|
||||||
|
export default function EmptyFill({ text, className }: EmptyFillProps) {
|
||||||
|
return (
|
||||||
|
<div className={cx([style.fill, className])}>
|
||||||
|
<Empty text={text} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
box-sizing: border-box; /* reset */
|
box-sizing: border-box; /* reset */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%; /* restrict the page width to viewport */
|
width: 100%; /* restrict the page width to viewport */
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
|
|
||||||
font-family: var(--font-family-override, $viewer-font-family);
|
font-family: var(--font-family-override, $viewer-font-family);
|
||||||
background: var(--background-color-override, $viewer-background-color);
|
background: var(--background-color-override, $viewer-background-color);
|
||||||
@@ -16,5 +16,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 5rem;
|
justify-content: center;
|
||||||
|
padding-block: 5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import style from './EmptyPage.module.scss';
|
|||||||
interface EmptyPageProps {
|
interface EmptyPageProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
injectedStyles?: CSSProperties;
|
injectedStyles?: CSSProperties;
|
||||||
|
variant?: 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmptyPage({ text, injectedStyles }: EmptyPageProps) {
|
export default function EmptyPage({ text, injectedStyles, variant }: EmptyPageProps) {
|
||||||
return (
|
return (
|
||||||
<div className={style.page}>
|
<div className={style.page}>
|
||||||
<Empty text={text} injectedStyles={injectedStyles} />
|
<Empty text={text} injectedStyles={injectedStyles} variant={variant} />
|
||||||
</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,15 +1,15 @@
|
|||||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import { useEntryActions, useScopedEntryActions } from '../hooks/useEntryAction';
|
import { useEntryActions } from '../hooks/useEntryAction';
|
||||||
import { useRundownSelectionContext } from './RundownSelectionContext';
|
|
||||||
|
|
||||||
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
||||||
const EntryActionsContext = createContext<EntryActionsContextValue | null>(null);
|
const EntryActionsContext = createContext<EntryActionsContextValue | null>(null);
|
||||||
|
|
||||||
export function EntryActionsProvider({ children }: PropsWithChildren) {
|
interface EntryActionsProviderProps extends PropsWithChildren {
|
||||||
const { effectiveRundownId } = useRundownSelectionContext();
|
actions: EntryActionsContextValue;
|
||||||
const actions = useScopedEntryActions(effectiveRundownId);
|
}
|
||||||
|
|
||||||
|
export function EntryActionsProvider({ children, actions }: EntryActionsProviderProps) {
|
||||||
return <EntryActionsContext.Provider value={actions}>{children}</EntryActionsContext.Provider>;
|
return <EntryActionsContext.Provider value={actions}>{children}</EntryActionsContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { Maybe, ProjectRundown } from 'ontime-types';
|
|
||||||
import { PropsWithChildren, createContext, startTransition, useCallback, useContext, useEffect, useMemo } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
import { useProjectRundowns } from '../hooks-query/useProjectRundowns';
|
|
||||||
|
|
||||||
export type RundownScopeValue = {
|
|
||||||
loadedRundownId: string;
|
|
||||||
selectedRundownId: Maybe<string>;
|
|
||||||
isLoadedRundown: boolean;
|
|
||||||
effectiveRundownId: string;
|
|
||||||
selectRundownId: (val: Maybe<string>) => void;
|
|
||||||
rundowns: ProjectRundown[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const RundownScopeContext = createContext<RundownScopeValue | null>(null);
|
|
||||||
|
|
||||||
export function RundownSelectionContextProvider({ children }: PropsWithChildren) {
|
|
||||||
'use memo';
|
|
||||||
const { data } = useProjectRundowns();
|
|
||||||
const { loaded, rundowns } = data;
|
|
||||||
|
|
||||||
const [selectedRundownId, setSelectedRundownId] = useSelectRundownFromParams();
|
|
||||||
|
|
||||||
const selectRundownId = useCallback(
|
|
||||||
(rundownId: Maybe<string>) => {
|
|
||||||
startTransition(() => {
|
|
||||||
if (rundowns.find((entry) => entry.id === rundownId)) setSelectedRundownId(rundownId);
|
|
||||||
else setSelectedRundownId(null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[rundowns, setSelectedRundownId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const effectiveRundownId = selectedRundownId ? selectedRundownId : loaded;
|
|
||||||
const isLoadedRundown = effectiveRundownId === loaded;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!rundowns.find((entry) => entry.id === effectiveRundownId)) setSelectedRundownId(null);
|
|
||||||
}, [rundowns, effectiveRundownId, setSelectedRundownId]);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
(): RundownScopeValue => ({
|
|
||||||
loadedRundownId: loaded,
|
|
||||||
isLoadedRundown,
|
|
||||||
selectedRundownId,
|
|
||||||
effectiveRundownId,
|
|
||||||
selectRundownId,
|
|
||||||
rundowns,
|
|
||||||
}),
|
|
||||||
[loaded, isLoadedRundown, selectedRundownId, effectiveRundownId, selectRundownId, rundowns],
|
|
||||||
);
|
|
||||||
|
|
||||||
return <RundownScopeContext.Provider value={value}>{children}</RundownScopeContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRundownSelectionContext() {
|
|
||||||
const context = useContext(RundownScopeContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useRundownScopeSelection requires a RundownSelectionContextProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rundownParam = 'rundownId';
|
|
||||||
|
|
||||||
export function useSelectRundownFromParams(): [Maybe<string>, (id: Maybe<string>) => void] {
|
|
||||||
'use memo';
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const selectedRundownId = searchParams.get(rundownParam);
|
|
||||||
const setSelectedRundownId = useCallback(
|
|
||||||
(id: Maybe<string>) => {
|
|
||||||
if (id === null) {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.delete(rundownParam);
|
|
||||||
return searchParams;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.set(rundownParam, id);
|
|
||||||
return searchParams;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setSearchParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
return [selectedRundownId, setSelectedRundownId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* mutates the provided `searchParams`
|
|
||||||
*/
|
|
||||||
export function setSelectRundownInParams(id: Maybe<string>, searchParams: URLSearchParams): void {
|
|
||||||
if (id === null) {
|
|
||||||
searchParams.delete(rundownParam);
|
|
||||||
} else {
|
|
||||||
searchParams.set(rundownParam, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDirectLinkToBackgroundEdit() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [search] = useSearchParams();
|
|
||||||
return useCallback(
|
|
||||||
async (rundownId: string) => {
|
|
||||||
setSelectRundownInParams(rundownId, search);
|
|
||||||
navigate({
|
|
||||||
pathname: '/cuesheet',
|
|
||||||
search: search.toString(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[navigate, search],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { useRundownSelectionContext } from '../context/RundownSelectionContext';
|
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
|
||||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
|
||||||
import { useFlatRundown, useRundown } from './useRundown';
|
|
||||||
|
|
||||||
export function useContextRundownEditModal() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId } = useRundownSelectionContext();
|
|
||||||
const { data: rundown } = useRundown(effectiveRundownId);
|
|
||||||
return { rundown };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextRundownCueRenumberModal() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId } = useRundownSelectionContext();
|
|
||||||
const { data } = useRundown(effectiveRundownId);
|
|
||||||
const { flatOrder } = data;
|
|
||||||
return { flatOrder };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextRundownList() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
|
||||||
const loadedEventId = useSelectedEventId();
|
|
||||||
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
|
||||||
const { data: rundown, status } = useRundown(effectiveRundownId);
|
|
||||||
const rundownMetadata = useMemo(
|
|
||||||
() => getRundownMetadata(rundown, effectiveSelectedEventId),
|
|
||||||
[effectiveSelectedEventId, rundown],
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
rundown,
|
|
||||||
rundownMetadata,
|
|
||||||
status,
|
|
||||||
isLoadedRundown,
|
|
||||||
}),
|
|
||||||
[rundown, rundownMetadata, status, isLoadedRundown],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextRundownTitleList() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
|
||||||
const loadedEventId = useSelectedEventId();
|
|
||||||
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
|
||||||
const { data: rundown, status } = useRundown(effectiveRundownId);
|
|
||||||
const flatRundown = useMemo(() => {
|
|
||||||
const flatData = getFlatRundownMetadata(rundown, effectiveSelectedEventId);
|
|
||||||
return flatData.filter(isOntimeEvent) as ExtendedEntry<OntimeEvent>[];
|
|
||||||
}, [effectiveSelectedEventId, rundown]);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
flatRundown,
|
|
||||||
status,
|
|
||||||
isLoadedRundown,
|
|
||||||
}),
|
|
||||||
[status, isLoadedRundown, flatRundown],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextRundownTable() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId, isLoadedRundown } = useRundownSelectionContext();
|
|
||||||
const loadedEventId = useSelectedEventId();
|
|
||||||
const effectiveSelectedEventId = isLoadedRundown ? loadedEventId : null;
|
|
||||||
const { data: rundown, status } = useRundown(effectiveRundownId);
|
|
||||||
const flatRundown = useMemo(
|
|
||||||
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
|
||||||
[effectiveSelectedEventId, rundown],
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
flatRundown,
|
|
||||||
status,
|
|
||||||
loadedEventId,
|
|
||||||
}),
|
|
||||||
[flatRundown, status, loadedEventId],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContextRundownFinder() {
|
|
||||||
'use memo';
|
|
||||||
const { effectiveRundownId } = useRundownSelectionContext();
|
|
||||||
const { data: rundown, status } = useFlatRundown(effectiveRundownId);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
rundown,
|
|
||||||
rundownId: effectiveRundownId,
|
|
||||||
status,
|
|
||||||
}),
|
|
||||||
[rundown, status, effectiveRundownId],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,12 @@ import { getCustomFields } from '../api/customFields';
|
|||||||
const placeholder: CustomFields = {};
|
const placeholder: CustomFields = {};
|
||||||
|
|
||||||
export default function useCustomFields() {
|
export default function useCustomFields() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
||||||
queryKey: CUSTOM_FIELDS,
|
queryKey: CUSTOM_FIELDS,
|
||||||
queryFn: ({ signal }) => getCustomFields({ signal }),
|
queryFn: ({ signal }) => getCustomFields({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? placeholder, status, isFetching, isError, refetch };
|
return { data: data ?? placeholder, status, isFetching, isError, isLoadingError, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { getProjectData, postProjectData } from '../api/project';
|
|||||||
import { projectDataPlaceholder } from '../models/ProjectData';
|
import { projectDataPlaceholder } from '../models/ProjectData';
|
||||||
|
|
||||||
export default function useProjectData() {
|
export default function useProjectData() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
||||||
queryKey: PROJECT_DATA,
|
queryKey: PROJECT_DATA,
|
||||||
queryFn: ({ signal }) => getProjectData({ signal }),
|
queryFn: ({ signal }) => getProjectData({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? projectDataPlaceholder, status, isFetching, isError, refetch };
|
return { data: data ?? projectDataPlaceholder, status, isFetching, isError, isLoadingError, refetch };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateProjectData() {
|
export function useUpdateProjectData() {
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { ProjectFile, ProjectFileList } from 'ontime-types';
|
import { ProjectFile, ProjectFileList, ProjectFileListResponse } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { PROJECT_LIST } from '../api/constants';
|
import { PROJECT_LIST } from '../api/constants';
|
||||||
import { getProjects } from '../api/db';
|
import { getProjects } from '../api/db';
|
||||||
|
|
||||||
export function useProjectList() {
|
const placeholderProjectList: ProjectFileListResponse = {
|
||||||
const { data, status, refetch } = useSuspenseQuery({
|
files: [],
|
||||||
|
lastLoadedProject: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function useProjectList() {
|
||||||
|
const { data, status, refetch } = useQuery({
|
||||||
queryKey: PROJECT_LIST,
|
queryKey: PROJECT_LIST,
|
||||||
queryFn: ({ signal }) => getProjects({ signal }),
|
queryFn: ({ signal }) => getProjects({ signal }),
|
||||||
staleTime: MILLIS_PER_HOUR,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
return { data, status, refetch };
|
return { data: data ?? placeholderProjectList, status, refetch };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectSortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'modified-asc' | 'modified-desc';
|
export type ProjectSortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'modified-asc' | 'modified-desc';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ProjectRundownsList } from 'ontime-types';
|
import { ProjectRundownsList } from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { PROJECT_RUNDOWNS } from '../api/constants';
|
import { PROJECT_RUNDOWNS } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
createRundown,
|
createRundown,
|
||||||
@@ -11,21 +11,24 @@ import {
|
|||||||
loadRundown,
|
loadRundown,
|
||||||
renameRundown,
|
renameRundown,
|
||||||
} from '../api/rundown';
|
} from '../api/rundown';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
|
|
||||||
|
//TODO: make suspends so we don't have to deal with no value all over
|
||||||
/**
|
/**
|
||||||
* Project rundowns
|
* Project rundowns
|
||||||
*/
|
*/
|
||||||
export function useProjectRundowns() {
|
export function useProjectRundowns() {
|
||||||
const { data, status, isError, refetch, isFetching } = useSuspenseQuery<ProjectRundownsList>({
|
const { data, status, isError, refetch, isFetching } = useQuery<ProjectRundownsList>({
|
||||||
queryKey: PROJECT_RUNDOWNS,
|
queryKey: PROJECT_RUNDOWNS,
|
||||||
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
queryFn: ({ signal }) => fetchProjectRundownList({ signal }),
|
||||||
staleTime: MILLIS_PER_HOUR,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
return { data, status, isError, refetch, isFetching };
|
return { data: data ?? { loaded: '', rundowns: [] }, status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMutateProjectRundowns() {
|
export function useMutateProjectRundowns() {
|
||||||
|
const ontimeQueryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutateAsync: create } = useMutation({
|
const { mutateAsync: create } = useMutation({
|
||||||
mutationFn: createRundown,
|
mutationFn: createRundown,
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { EntryId, Maybe, OntimeEntry, Rundown } from 'ontime-types';
|
import { EntryId, OntimeEntry, Rundown } from 'ontime-types';
|
||||||
import { useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { getRundownQueryKey } from '../api/constants';
|
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
|
||||||
import { fetchRundown } from '../api/rundown';
|
import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
|
||||||
import { useSelectedEventId } from '../hooks/useSocket';
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
|
||||||
import { useProjectRundowns } from './useProjectRundowns';
|
import { useProjectRundowns } from './useProjectRundowns';
|
||||||
@@ -20,45 +20,52 @@ const cachedRundownPlaceholder: Rundown = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to a specific rundown by ID.
|
* Normalised rundown data for the currently loaded rundown.
|
||||||
* When rundownId is not provided the loaded rundown is provided
|
*
|
||||||
|
* Bootstraps via the `/current` alias so the first paint is a single round-trip,
|
||||||
|
* independent of the project rundown list. Once the loaded id is known, the
|
||||||
|
* query key swaps to the id-keyed cache that is shared with `useRundownById`.
|
||||||
*/
|
*/
|
||||||
export function useRundown(rundownId: Maybe<string>) {
|
export default function useRundown() {
|
||||||
'use memo';
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { loaded: loadedRundownId },
|
data: { loaded: loadedRundownId },
|
||||||
} = useProjectRundowns();
|
} = useProjectRundowns();
|
||||||
|
|
||||||
const effectiveRundownId = rundownId !== null ? rundownId : loadedRundownId;
|
const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
const isLoadedRundown = effectiveRundownId === loadedRundownId;
|
queryKey: loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY,
|
||||||
|
queryFn: ({ signal }) => fetchCurrentRundown({ signal }),
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
|
|
||||||
queryKey: getRundownQueryKey(effectiveRundownId),
|
|
||||||
queryFn: ({ signal }) => fetchRundown(effectiveRundownId, { signal }),
|
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? cachedRundownPlaceholder, status, isError, refetch, isFetching, isLoadedRundown };
|
// Seed the id-keyed cache when fetching via the bootstrap alias
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data || loadedRundownId) return;
|
||||||
|
queryClient.setQueryData(getRundownQueryKey(data.id), data);
|
||||||
|
}, [data, loadedRundownId, queryClient]);
|
||||||
|
|
||||||
|
// Once we have the ID, drop the temporary current cache
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loadedRundownId) return;
|
||||||
|
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
||||||
|
}, [loadedRundownId, queryClient]);
|
||||||
|
|
||||||
|
return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRundownWithMetadata(rundownId: Maybe<string>) {
|
export function useRundownWithMetadata() {
|
||||||
'use memo';
|
const { data, status, isLoadingError } = useRundown();
|
||||||
|
|
||||||
const { data, status, isLoadedRundown } = useRundown(rundownId);
|
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const effectiveSelectedEventId = isLoadedRundown ? selectedEventId : null;
|
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
||||||
const rundownMetadata = getRundownMetadata(data, effectiveSelectedEventId);
|
return { data, status, isLoadingError, rundownMetadata };
|
||||||
return { data, status, rundownMetadata };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to a flat rundown
|
* Provides access to a flat rundown
|
||||||
* built from the order and rundown fields
|
* built from the order and rundown fields
|
||||||
*/
|
*/
|
||||||
export function useFlatRundown(rundownId: Maybe<string>) {
|
export function useFlatRundown() {
|
||||||
const { data, status } = useRundown(rundownId);
|
const { data, status, isLoadingError } = useRundown();
|
||||||
|
|
||||||
const flatRundown = useMemo(() => {
|
const flatRundown = useMemo(() => {
|
||||||
if (data.revision === -1) {
|
if (data.revision === -1) {
|
||||||
@@ -67,17 +74,15 @@ export function useFlatRundown(rundownId: Maybe<string>) {
|
|||||||
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(rundownId: Maybe<string>) {
|
export function useFlatRundownWithMetadata() {
|
||||||
'use memo';
|
const { data, status, isLoadingError } = useRundown();
|
||||||
|
|
||||||
const { data, status, isLoadedRundown } = useRundown(rundownId);
|
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const effectiveSelectedEventId = isLoadedRundown ? selectedEventId : null;
|
|
||||||
const rundownWithMetadata = getFlatRundownMetadata(data, effectiveSelectedEventId);
|
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
|
||||||
return { data: rundownWithMetadata, status };
|
return { data: rundownWithMetadata, status, isLoadingError };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,8 +92,8 @@ export function useFlatRundownWithMetadata(rundownId: Maybe<string>) {
|
|||||||
* re-filtering on every render.
|
* re-filtering on every render.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function usePartialRundown(rundownId: Maybe<string>, cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
export function usePartialRundown(cb: (event: ExtendedEntry<OntimeEntry>) => boolean) {
|
||||||
const { data, status } = useFlatRundownWithMetadata(rundownId);
|
const { data, status } = useFlatRundownWithMetadata();
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
return data.filter(cb);
|
return data.filter(cb);
|
||||||
}, [data, cb]);
|
}, [data, cb]);
|
||||||
@@ -98,20 +103,37 @@ export function usePartialRundown(rundownId: Maybe<string>, cb: (event: Extended
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to get a specific entry by ID from the rundown
|
* Hook to get a specific entry by ID from the rundown
|
||||||
* @deprecated
|
|
||||||
*/
|
*/
|
||||||
export function useEntry(rundownId: Maybe<string>, entryId: EntryId | null): OntimeEntry | null {
|
export function useEntry(entryId: EntryId | null): OntimeEntry | null {
|
||||||
const { data: rundown } = useRundown(rundownId);
|
const { data: rundown } = useRundown();
|
||||||
|
|
||||||
if (entryId === null) return null;
|
if (entryId === null) return null;
|
||||||
return rundown.entries[entryId] ?? null;
|
return rundown.entries[entryId] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRundownAuxData(rundownId: Maybe<string>) {
|
export function useRundownAuxData() {
|
||||||
const { data, status } = useRundown(rundownId);
|
const { data, status } = useRundown();
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
const { title, id } = data;
|
const { title, id } = data;
|
||||||
return { title, id };
|
return { title, id };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return { data: filteredData, status };
|
return { data: filteredData, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to a specific rundown by ID.
|
||||||
|
* When rundownId is null/undefined the query is disabled and returns the placeholder.
|
||||||
|
*/
|
||||||
|
export function useRundownById(rundownId: string | null | undefined) {
|
||||||
|
const enabled = Boolean(rundownId);
|
||||||
|
|
||||||
|
const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery<Rundown>({
|
||||||
|
queryKey: getRundownQueryKey(rundownId ?? ''),
|
||||||
|
queryFn: ({ signal }) => fetchRundown(rundownId!, { signal }),
|
||||||
|
enabled,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { EntryId, Rundown } from 'ontime-types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useSelectedEventId } from '../hooks/useSocket';
|
||||||
|
import { getFlatRundownMetadata, type ExtendedEntry } from '../utils/rundownMetadata';
|
||||||
|
import { useProjectRundowns } from './useProjectRundowns';
|
||||||
|
import { useRundownById } from './useRundown';
|
||||||
|
|
||||||
|
export type RundownSource = {
|
||||||
|
rundownId: string | null;
|
||||||
|
rundown: Rundown;
|
||||||
|
flatRundown: ExtendedEntry[];
|
||||||
|
status: string;
|
||||||
|
isLoadingError: boolean;
|
||||||
|
selectedEventId: EntryId | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly scoped rundown data for views that may operate on a non-loaded rundown.
|
||||||
|
*/
|
||||||
|
export function useScopedRundown(rundownId: string | null): RundownSource {
|
||||||
|
const { data: projectRundowns } = useProjectRundowns();
|
||||||
|
return useRundownSource(rundownId, projectRundowns.loaded || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loaded-rundown source for views that must follow the active runtime rundown.
|
||||||
|
*/
|
||||||
|
export function useLoadedRundownSource(): RundownSource {
|
||||||
|
const { data: projectRundowns } = useProjectRundowns();
|
||||||
|
const loadedRundownId = projectRundowns.loaded || null;
|
||||||
|
return useRundownSource(loadedRundownId, loadedRundownId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRundownSource(rundownId: string | null, loadedRundownId: string | null): RundownSource {
|
||||||
|
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
|
||||||
|
const runtimeSelectedEventId = useSelectedEventId();
|
||||||
|
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
|
||||||
|
const { data: rundown, status, isLoadingError } = useRundownById(rundownId);
|
||||||
|
const flatRundown = useMemo(
|
||||||
|
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
|
||||||
|
[effectiveSelectedEventId, rundown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
rundownId,
|
||||||
|
rundown,
|
||||||
|
flatRundown,
|
||||||
|
status,
|
||||||
|
isLoadingError,
|
||||||
|
selectedEventId: effectiveSelectedEventId,
|
||||||
|
}),
|
||||||
|
[effectiveSelectedEventId, flatRundown, isLoadingError, rundown, rundownId, status],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { getSettings } from '../api/settings';
|
|||||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||||
|
|
||||||
export default function useSettings() {
|
export default function useSettings() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
|
||||||
queryKey: APP_SETTINGS,
|
queryKey: APP_SETTINGS,
|
||||||
queryFn: ({ signal }) => getSettings({ signal }),
|
queryFn: ({ signal }) => getSettings({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
@@ -22,5 +22,5 @@ export default function useSettings() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, refetch };
|
return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, isLoadingError, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { VIEW_SETTINGS } from '../api/constants';
|
|||||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||||
|
|
||||||
export default function useViewSettings() {
|
export default function useViewSettings() {
|
||||||
const { data, status } = useQuery({
|
const { data, status, isLoadingError } = useQuery({
|
||||||
queryKey: VIEW_SETTINGS,
|
queryKey: VIEW_SETTINGS,
|
||||||
queryFn: ({ signal }) => getViewSettings({ signal }),
|
queryFn: ({ signal }) => getViewSettings({ signal }),
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
@@ -24,5 +24,5 @@ export default function useViewSettings() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data: data ?? viewsSettingsPlaceholder, status, mutateAsync };
|
return { data: data ?? viewsSettingsPlaceholder, status, isLoadingError, mutateAsync };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 0,
|
eventIndex: 0,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -108,7 +107,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1, // UI indexes are 1 based
|
eventIndex: 1, // UI indexes are 1 based
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -126,7 +124,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1,
|
eventIndex: 1,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: true,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -143,7 +140,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -160,7 +156,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: true,
|
isPast: true,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -177,7 +172,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 3,
|
eventIndex: 3,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: true,
|
isLinkedToLoaded: true,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
@@ -194,7 +188,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 4,
|
eventIndex: 4,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: true,
|
isLinkedToLoaded: true,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -211,7 +204,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 5,
|
eventIndex: 5,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 7,
|
totalGap: 7,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -264,7 +256,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 0,
|
eventIndex: 0,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -282,7 +273,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 1,
|
eventIndex: 1,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
@@ -300,7 +290,6 @@ describe('initRundownMetadata()', () => {
|
|||||||
eventIndex: 2,
|
eventIndex: 2,
|
||||||
isPast: false,
|
isPast: false,
|
||||||
isNextDay: false,
|
isNextDay: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
totalGap: 0,
|
totalGap: 0,
|
||||||
isLinkedToLoaded: false,
|
isLinkedToLoaded: false,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export type RundownMetadata = {
|
|||||||
groupColour: string | undefined;
|
groupColour: string | undefined;
|
||||||
groupEntries: number | undefined;
|
groupEntries: number | undefined;
|
||||||
isFirstAfterGroup: boolean;
|
isFirstAfterGroup: boolean;
|
||||||
isParentToLoaded: boolean; // if the group contains the loaded event
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExtendedEntry<T extends OntimeEntry = OntimeEntry> = T & RundownMetadata;
|
export type ExtendedEntry<T extends OntimeEntry = OntimeEntry> = T & RundownMetadata;
|
||||||
@@ -95,7 +94,6 @@ export function initRundownMetadata(selectedEventId: MaybeString) {
|
|||||||
groupColour: undefined,
|
groupColour: undefined,
|
||||||
groupEntries: undefined,
|
groupEntries: undefined,
|
||||||
isFirstAfterGroup: false,
|
isFirstAfterGroup: false,
|
||||||
isParentToLoaded: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
||||||
@@ -119,7 +117,6 @@ function processEntry(
|
|||||||
// initialise data to be overridden below
|
// initialise data to be overridden below
|
||||||
processedData.isNextDay = false;
|
processedData.isNextDay = false;
|
||||||
processedData.isLoaded = false;
|
processedData.isLoaded = false;
|
||||||
processedData.isParentToLoaded = false;
|
|
||||||
|
|
||||||
processedData.previousEntryId = processedData.thisId; // thisId comes from the previous iteration
|
processedData.previousEntryId = processedData.thisId; // thisId comes from the previous iteration
|
||||||
processedData.thisId = entry.id; // we reassign thisId
|
processedData.thisId = entry.id; // we reassign thisId
|
||||||
@@ -135,7 +132,6 @@ function processEntry(
|
|||||||
processedData.groupId = entry.id;
|
processedData.groupId = entry.id;
|
||||||
processedData.groupColour = entry.colour;
|
processedData.groupColour = entry.colour;
|
||||||
processedData.groupEntries = entry.entries.length;
|
processedData.groupEntries = entry.entries.length;
|
||||||
processedData.isParentToLoaded = selectedEventId ? entry.entries.includes(selectedEventId) : false;
|
|
||||||
} else {
|
} else {
|
||||||
// for delays and groups, we insert the group metadata
|
// for delays and groups, we insert the group metadata
|
||||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
APP_SETTINGS,
|
APP_SETTINGS,
|
||||||
CLIENT_LIST,
|
CLIENT_LIST,
|
||||||
CSS_OVERRIDE,
|
CSS_OVERRIDE,
|
||||||
|
CURRENT_RUNDOWN_QUERY_KEY,
|
||||||
CUSTOM_FIELDS,
|
CUSTOM_FIELDS,
|
||||||
PROJECT_DATA,
|
PROJECT_DATA,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -24,7 +25,6 @@ import {
|
|||||||
VIEW_SETTINGS,
|
VIEW_SETTINGS,
|
||||||
getRundownQueryKey,
|
getRundownQueryKey,
|
||||||
PROJECT_RUNDOWNS,
|
PROJECT_RUNDOWNS,
|
||||||
PROJECT_LIST,
|
|
||||||
} from '../api/constants';
|
} from '../api/constants';
|
||||||
import { invalidateAllCaches } from '../api/utils';
|
import { invalidateAllCaches } from '../api/utils';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
@@ -216,9 +216,6 @@ export const connectSocket = () => {
|
|||||||
case RefetchKey.ProjectRundowns:
|
case RefetchKey.ProjectRundowns:
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_RUNDOWNS });
|
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_RUNDOWNS });
|
||||||
break;
|
break;
|
||||||
case RefetchKey.ProjectFiles:
|
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
|
||||||
break;
|
|
||||||
default: {
|
default: {
|
||||||
target satisfies never;
|
target satisfies never;
|
||||||
break;
|
break;
|
||||||
@@ -241,6 +238,7 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s
|
|||||||
if (!rundownId) {
|
if (!rundownId) {
|
||||||
// we omit rundownId to signify invalidate all rundowns
|
// we omit rundownId to signify invalidate all rundowns
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +252,12 @@ export function maybeInvalidateRundownCache(revision: MaybeNumber, rundownId?: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey, exact: true });
|
ontimeQueryClient.invalidateQueries({ queryKey, exact: true });
|
||||||
|
|
||||||
|
// keep current alias in sync with the ID-based cache
|
||||||
|
const loadedRundownId = ontimeQueryClient.getQueryData<{ loaded: string }>(PROJECT_RUNDOWNS)?.loaded;
|
||||||
|
if (!loadedRundownId || loadedRundownId === rundownId) {
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendSocket<T extends MessageTag | ApiActionTag>(
|
export function sendSocket<T extends MessageTag | ApiActionTag>(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { deleteAllReport } from '../../../../common/api/report';
|
|||||||
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import useReport from '../../../../common/hooks-query/useReport';
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import { useRundown } from '../../../../common/hooks-query/useRundown';
|
import useRundown from '../../../../common/hooks-query/useRundown';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../../common/utils/time';
|
import { formatTime } from '../../../../common/utils/time';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -15,7 +15,7 @@ import style from './ReportSettings.module.scss';
|
|||||||
|
|
||||||
export default function ReportSettings() {
|
export default function ReportSettings() {
|
||||||
const { data: reportData } = useReport();
|
const { data: reportData } = useReport();
|
||||||
const { data } = useRundown(null);
|
const { data } = useRundown();
|
||||||
|
|
||||||
const clearReport = async () => await deleteAllReport();
|
const clearReport = async () => await deleteAllReport();
|
||||||
const downloadCSV = (combinedReport: CombinedReport[]) => {
|
const downloadCSV = (combinedReport: CombinedReport[]) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { Suspense, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IoAdd,
|
IoAdd,
|
||||||
IoDocumentOutline,
|
IoDocumentOutline,
|
||||||
@@ -18,9 +18,9 @@ import IconButton from '../../../../common/components/buttons/IconButton';
|
|||||||
import Dialog from '../../../../common/components/dialog/Dialog';
|
import Dialog from '../../../../common/components/dialog/Dialog';
|
||||||
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import { useDirectLinkToBackgroundEdit } from '../../../../common/context/RundownSelectionContext';
|
|
||||||
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
import { useMutateProjectRundowns, useProjectRundowns } from '../../../../common/hooks-query/useProjectRundowns';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
|
import { useDirectLinkToBackgroundEdit } from '../../../../views/cuesheet/useCuesheetRundownSelection';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import RundownRenameForm from './composite/RundownRenameForm';
|
import RundownRenameForm from './composite/RundownRenameForm';
|
||||||
import { ManageRundownForm } from './ManageRundownForm';
|
import { ManageRundownForm } from './ManageRundownForm';
|
||||||
@@ -28,20 +28,6 @@ import { ManageRundownForm } from './ManageRundownForm';
|
|||||||
import style from './ManagePanel.module.scss';
|
import style from './ManagePanel.module.scss';
|
||||||
|
|
||||||
export default function ManageRundowns() {
|
export default function ManageRundowns() {
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className={style.empty}>
|
|
||||||
<Panel.Loader isLoading />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ManageRundownsSuspense />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ManageRundownsSuspense() {
|
|
||||||
const { data } = useProjectRundowns();
|
const { data } = useProjectRundowns();
|
||||||
const { duplicate, remove, load, rename } = useMutateProjectRundowns();
|
const { duplicate, remove, load, rename } = useMutateProjectRundowns();
|
||||||
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
const [isOpenDelete, deleteHandlers] = useDisclosure();
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ import Button from '../../../../../common/components/buttons/Button';
|
|||||||
import Info from '../../../../../common/components/info/Info';
|
import Info from '../../../../../common/components/info/Info';
|
||||||
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
|
||||||
import Modal from '../../../../../common/components/modal/Modal';
|
import Modal from '../../../../../common/components/modal/Modal';
|
||||||
import { useRundown } from '../../../../../common/hooks-query/useRundown';
|
import useRundown from '../../../../../common/hooks-query/useRundown';
|
||||||
import { removeFileExtension, validateExcelImport } from '../../../../../common/utils/uploadUtils';
|
import { removeFileExtension, validateExcelImport } from '../../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||||
import GSheetSetup from './GSheetSetup';
|
import GSheetSetup from './GSheetSetup';
|
||||||
@@ -59,7 +59,7 @@ export default function SourcesPanel() {
|
|||||||
const [activeSource, setActiveSource] = useState<ActiveSource | null>(null);
|
const [activeSource, setActiveSource] = useState<ActiveSource | null>(null);
|
||||||
const [completedRundownTitle, setCompletedRundownTitle] = useState('');
|
const [completedRundownTitle, setCompletedRundownTitle] = useState('');
|
||||||
|
|
||||||
const { data: currentRundown } = useRundown(null);
|
const { data: currentRundown } = useRundown();
|
||||||
const { applyImport } = useSpreadsheetImport();
|
const { applyImport } = useSpreadsheetImport();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoArrowDown, IoArrowUp } from 'react-icons/io5';
|
import { IoArrowDown, IoArrowUp } from 'react-icons/io5';
|
||||||
|
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
@@ -11,25 +11,11 @@ import style from './ProjectPanel.module.scss';
|
|||||||
type SortParameter = 'alphabetical' | 'modified';
|
type SortParameter = 'alphabetical' | 'modified';
|
||||||
|
|
||||||
export default function ProjectList() {
|
export default function ProjectList() {
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className={style.empty}>
|
|
||||||
<Panel.Loader isLoading />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ProjectListSuspend />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectListSuspend() {
|
|
||||||
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
|
||||||
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
||||||
const [sortMode, setSortMode] = useState<ProjectSortMode>('modified-desc');
|
const [sortMode, setSortMode] = useState<ProjectSortMode>('modified-desc');
|
||||||
|
|
||||||
const { data, refetch } = useOrderedProjectList(sortMode);
|
const { data, refetch, status } = useOrderedProjectList(sortMode);
|
||||||
|
|
||||||
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
const handleToggleEditMode = (editMode: EditMode, filename: string | null) => {
|
||||||
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
|
setEditingMode((prev) => (prev === editMode && filename === editingFilename ? null : editMode));
|
||||||
@@ -52,6 +38,14 @@ function ProjectListSuspend() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return (
|
||||||
|
<div className={style.empty}>
|
||||||
|
<Panel.Loader isLoading />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const numProjects = data.reorderedProjectFiles.length;
|
const numProjects = data.reorderedProjectFiles.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 EmptyFill from '../../common/components/state/EmptyFill';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
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';
|
||||||
@@ -10,6 +11,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 { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Loader from '../../views/common/loader/Loader';
|
import Loader from '../../views/common/loader/Loader';
|
||||||
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';
|
||||||
@@ -35,7 +37,7 @@ export default function OperatorLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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 <Operator {...data} />;
|
return <Operator {...data} />;
|
||||||
@@ -43,6 +45,7 @@ export default function OperatorLoader() {
|
|||||||
|
|
||||||
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
|
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions();
|
const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions();
|
||||||
|
|
||||||
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
||||||
@@ -113,6 +116,7 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
|
|||||||
const operatorOptions = useMemo(() => getOperatorOptions(customFields, defaultFormat), [customFields, defaultFormat]);
|
const operatorOptions = useMemo(() => getOperatorOptions(customFields, defaultFormat), [customFields, defaultFormat]);
|
||||||
|
|
||||||
const canEdit = shouldEdit && subscribe.length;
|
const canEdit = shouldEdit && subscribe.length;
|
||||||
|
const hasEvents = rundown.order.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.operatorContainer} data-testid='operator-view'>
|
<div className={style.operatorContainer} data-testid='operator-view'>
|
||||||
@@ -127,117 +131,121 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
{!hasEvents ? (
|
||||||
{rundown.order.map((entryId) => {
|
<EmptyFill text={getLocalizedString('common.no_data')} />
|
||||||
const entry = rundown.entries[entryId];
|
) : (
|
||||||
if (isOntimeEvent(entry)) {
|
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||||
const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
|
{rundown.order.map((entryId) => {
|
||||||
// hide past events (if setting) and skipped events
|
const entry = rundown.entries[entryId];
|
||||||
if ((hidePast && isPast) || entry.skip) {
|
if (isOntimeEvent(entry)) {
|
||||||
return null;
|
const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
|
||||||
}
|
// hide past events (if setting) and skipped events
|
||||||
|
if ((hidePast && isPast) || entry.skip) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { mainField, secondaryField, subscribedData } = getEventData(
|
const { mainField, secondaryField, subscribedData } = getEventData(
|
||||||
entry,
|
entry,
|
||||||
mainSource,
|
mainSource,
|
||||||
secondarySource,
|
secondarySource,
|
||||||
subscribe,
|
subscribe,
|
||||||
customFields,
|
customFields,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperatorEvent
|
<OperatorEvent
|
||||||
key={entry.id}
|
|
||||||
id={entry.id}
|
|
||||||
colour={entry.colour}
|
|
||||||
cue={entry.cue}
|
|
||||||
main={mainField}
|
|
||||||
secondary={secondaryField}
|
|
||||||
timeStart={entry.timeStart}
|
|
||||||
duration={entry.duration}
|
|
||||||
delay={entry.delay}
|
|
||||||
dayOffset={entry.dayOffset}
|
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
|
||||||
isSelected={isLoaded}
|
|
||||||
isPast={isPast}
|
|
||||||
selectedRef={isLoaded ? selectedRef : undefined}
|
|
||||||
showStart={showStart}
|
|
||||||
subscribed={subscribedData}
|
|
||||||
totalGap={totalGap}
|
|
||||||
onLongPress={canEdit ? handleEdit : () => undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeGroup(entry)) {
|
|
||||||
const { isPast } = rundownMetadata[entry.id];
|
|
||||||
|
|
||||||
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
|
|
||||||
|
|
||||||
if (hidePast && isPast && !isCurrentParent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={entry.id}>
|
|
||||||
<OperatorGroup
|
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
title={entry.title}
|
id={entry.id}
|
||||||
colour={entry.colour}
|
colour={entry.colour}
|
||||||
count={entry.entries.length}
|
cue={entry.cue}
|
||||||
|
main={mainField}
|
||||||
|
secondary={secondaryField}
|
||||||
|
timeStart={entry.timeStart}
|
||||||
duration={entry.duration}
|
duration={entry.duration}
|
||||||
|
delay={entry.delay}
|
||||||
|
dayOffset={entry.dayOffset}
|
||||||
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
|
isSelected={isLoaded}
|
||||||
|
isPast={isPast}
|
||||||
|
selectedRef={isLoaded ? selectedRef : undefined}
|
||||||
|
showStart={showStart}
|
||||||
|
subscribed={subscribedData}
|
||||||
|
totalGap={totalGap}
|
||||||
|
onLongPress={canEdit ? handleEdit : () => undefined}
|
||||||
/>
|
/>
|
||||||
{entry.entries.map((nestedEntryId) => {
|
);
|
||||||
const nestedEntry = rundown.entries[nestedEntryId];
|
}
|
||||||
if (!isOntimeEvent(nestedEntry)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId];
|
if (isOntimeGroup(entry)) {
|
||||||
|
const { isPast } = rundownMetadata[entry.id];
|
||||||
|
|
||||||
// hide past events (if setting) and skipped events
|
const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
|
||||||
if ((hidePast && isPast) || nestedEntry.skip) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { mainField, secondaryField, subscribedData } = getEventData(
|
if (hidePast && isPast && !isCurrentParent) {
|
||||||
nestedEntry,
|
return null;
|
||||||
mainSource,
|
}
|
||||||
secondarySource,
|
|
||||||
subscribe,
|
|
||||||
customFields,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperatorEvent
|
<Fragment key={entry.id}>
|
||||||
key={nestedEntry.id}
|
<OperatorGroup
|
||||||
id={nestedEntry.id}
|
key={entry.id}
|
||||||
colour={nestedEntry.colour}
|
title={entry.title}
|
||||||
cue={nestedEntry.cue}
|
colour={entry.colour}
|
||||||
main={mainField}
|
count={entry.entries.length}
|
||||||
secondary={secondaryField}
|
duration={entry.duration}
|
||||||
timeStart={nestedEntry.timeStart}
|
/>
|
||||||
duration={nestedEntry.duration}
|
{entry.entries.map((nestedEntryId) => {
|
||||||
delay={nestedEntry.delay}
|
const nestedEntry = rundown.entries[nestedEntryId];
|
||||||
dayOffset={nestedEntry.dayOffset}
|
if (!isOntimeEvent(nestedEntry)) {
|
||||||
isLinkedToLoaded={isLinkedToLoaded}
|
return null;
|
||||||
isSelected={isLoaded}
|
}
|
||||||
isPast={isPast}
|
|
||||||
groupColour={entry.colour}
|
const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId];
|
||||||
selectedRef={isLoaded ? selectedRef : undefined}
|
|
||||||
showStart={showStart}
|
// hide past events (if setting) and skipped events
|
||||||
subscribed={subscribedData}
|
if ((hidePast && isPast) || nestedEntry.skip) {
|
||||||
totalGap={totalGap}
|
return null;
|
||||||
onLongPress={canEdit ? handleEdit : () => undefined}
|
}
|
||||||
/>
|
|
||||||
);
|
const { mainField, secondaryField, subscribedData } = getEventData(
|
||||||
})}
|
nestedEntry,
|
||||||
</Fragment>
|
mainSource,
|
||||||
);
|
secondarySource,
|
||||||
}
|
subscribe,
|
||||||
return null;
|
customFields,
|
||||||
})}
|
);
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<OperatorEvent
|
||||||
|
key={nestedEntry.id}
|
||||||
|
id={nestedEntry.id}
|
||||||
|
colour={nestedEntry.colour}
|
||||||
|
cue={nestedEntry.cue}
|
||||||
|
main={mainField}
|
||||||
|
secondary={secondaryField}
|
||||||
|
timeStart={nestedEntry.timeStart}
|
||||||
|
duration={nestedEntry.duration}
|
||||||
|
delay={nestedEntry.delay}
|
||||||
|
dayOffset={nestedEntry.dayOffset}
|
||||||
|
isLinkedToLoaded={isLinkedToLoaded}
|
||||||
|
isSelected={isLoaded}
|
||||||
|
isPast={isPast}
|
||||||
|
groupColour={entry.colour}
|
||||||
|
selectedRef={isLoaded ? selectedRef : undefined}
|
||||||
|
showStart={showStart}
|
||||||
|
subscribed={subscribedData}
|
||||||
|
totalGap={totalGap}
|
||||||
|
onLongPress={canEdit ? handleEdit : () => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<FollowButton isVisible={lockAutoScroll} onClickHandler={handleOffset} />
|
<FollowButton isVisible={lockAutoScroll} onClickHandler={handleOffset} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,18 @@ export interface OperatorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useOperatorData(): ViewData<OperatorData> {
|
export function useOperatorData(): ViewData<OperatorData> {
|
||||||
const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata(null);
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ErrorBoundary } from '@sentry/react';
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
import { PropsWithChildren, ReactNode, Suspense } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import ScrollArea from '../../common/components/scroll-area/ScrollArea';
|
import ScrollArea from '../../common/components/scroll-area/ScrollArea';
|
||||||
import { useIsOnline } from '../../common/hooks/useSocket';
|
import { useIsOnline } from '../../common/hooks/useSocket';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
import Loader from '../../views/common/loader/Loader';
|
|
||||||
|
|
||||||
import style from './Overview.module.scss';
|
import style from './Overview.module.scss';
|
||||||
|
|
||||||
@@ -16,37 +15,18 @@ export function OverviewWrapper({ navElements, children }: PropsWithChildren<Ove
|
|||||||
const isOnline = useIsOnline();
|
const isOnline = useIsOnline();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<OverviewFallback navElements={navElements} />}>
|
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
||||||
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<div className={style.nav}>{navElements}</div>
|
||||||
<div className={style.nav}>{navElements}</div>
|
<ScrollArea
|
||||||
<ScrollArea
|
className={style.infoScroll}
|
||||||
className={style.infoScroll}
|
contentClassName={style.info}
|
||||||
contentClassName={style.info}
|
contentStyle={{ minWidth: '100%' }}
|
||||||
contentStyle={{ minWidth: '100%' }}
|
orientation='horizontal'
|
||||||
orientation='horizontal'
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</ScrollArea>
|
||||||
</ScrollArea>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OverviewFallback({ navElements }: OverviewWrapperProps) {
|
|
||||||
return (
|
|
||||||
<div className={style.overview}>
|
|
||||||
<div className={style.nav}>{navElements}</div>
|
|
||||||
<ScrollArea
|
|
||||||
className={style.infoScroll}
|
|
||||||
contentClassName={style.info}
|
|
||||||
contentStyle={{ minWidth: '100%' }}
|
|
||||||
orientation='horizontal'
|
|
||||||
>
|
|
||||||
{/* TODO: this could be alined in a nicer way */}
|
|
||||||
<Loader />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export function MetadataTimes() {
|
|||||||
function GroupTimes() {
|
function GroupTimes() {
|
||||||
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback, phase } = useGroupTimerOverView();
|
const { clock, mode, groupExpectedEnd, actualGroupStart, currentDay, playback, phase } = useGroupTimerOverView();
|
||||||
const currentGroupId = useCurrentGroupId();
|
const currentGroupId = useCurrentGroupId();
|
||||||
const group = useEntry(null, currentGroupId) as OntimeGroup | null;
|
const group = useEntry(currentGroupId) as OntimeGroup | null;
|
||||||
|
|
||||||
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ function GroupTimes() {
|
|||||||
function FlagTimes() {
|
function FlagTimes() {
|
||||||
const { clock, mode, actualStart, plannedStart, playback, currentDay, phase } = useFlagTimerOverView();
|
const { clock, mode, actualStart, plannedStart, playback, currentDay, phase } = useFlagTimerOverView();
|
||||||
const { id, expectedStart } = useNextFlag();
|
const { id, expectedStart } = useNextFlag();
|
||||||
const entry = useEntry(null, id) as OntimeEvent | null;
|
const entry = useEntry(id) as OntimeEvent | null;
|
||||||
|
|
||||||
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
const hasRunningTimer = phase !== TimerPhase.Pending && isPlaybackActive(playback);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import style from './TitleOverview.module.scss';
|
|||||||
export default function TitleOverview() {
|
export default function TitleOverview() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const { data: projectData } = useProjectData();
|
const { data: projectData } = useProjectData();
|
||||||
const { data: rundownData } = useRundownAuxData(null);
|
const { data: rundownData } = useRundownAuxData();
|
||||||
|
|
||||||
if (!projectData.title && !rundownData.title) {
|
if (!projectData.title && !rundownData.title) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
|||||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||||
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
||||||
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
import { useLoadedRundownSource } from '../../common/hooks-query/useScopedRundown';
|
||||||
|
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||||
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
|
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
|
||||||
import { handleLinks } from '../../common/utils/linkUtils';
|
import { handleLinks } from '../../common/utils/linkUtils';
|
||||||
import { cx } from '../../common/utils/styleUtils';
|
import { cx } from '../../common/utils/styleUtils';
|
||||||
@@ -38,29 +39,28 @@ function RundownExport() {
|
|||||||
defaultValue: RundownViewMode.List,
|
defaultValue: RundownViewMode.List,
|
||||||
});
|
});
|
||||||
const isSmallDevice = useIsSmallDevice();
|
const isSmallDevice = useIsSmallDevice();
|
||||||
|
const entryActions = useEntryActions();
|
||||||
|
|
||||||
if (isSmallDevice && isExtracted) {
|
if (isSmallDevice && isExtracted) {
|
||||||
return (
|
return (
|
||||||
<RundownSelectionContextProvider>
|
<EntryActionsProvider actions={entryActions}>
|
||||||
<EntryActionsProvider>
|
<ProtectRoute permission='editor'>
|
||||||
<ProtectRoute permission='editor'>
|
<div
|
||||||
<div
|
className={cx([style.rundownExport, style.extracted])}
|
||||||
className={cx([style.rundownExport, style.extracted])}
|
data-target='small-device'
|
||||||
data-target='small-device'
|
data-testid='panel-rundown'
|
||||||
data-testid='panel-rundown'
|
>
|
||||||
>
|
<FinderPlacement />
|
||||||
<FinderPlacement />
|
<ViewNavigationMenu suppressSettings />
|
||||||
<ViewNavigationMenu suppressSettings />
|
<div className={style.rundown}>
|
||||||
<div className={style.rundown}>
|
<ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<RundownRoot isSmallDevice isExtracted viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
<RundownRoot isSmallDevice isExtracted viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownContextMenu />
|
||||||
<RundownContextMenu />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ProtectRoute>
|
</div>
|
||||||
</EntryActionsProvider>
|
</ProtectRoute>
|
||||||
</RundownSelectionContextProvider>
|
</EntryActionsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,32 +70,30 @@ function RundownExport() {
|
|||||||
viewMode === RundownViewMode.Table;
|
viewMode === RundownViewMode.Table;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RundownSelectionContextProvider>
|
<EntryActionsProvider actions={entryActions}>
|
||||||
<EntryActionsProvider>
|
<ProtectRoute permission='editor'>
|
||||||
<ProtectRoute permission='editor'>
|
<div className={cx([style.rundownExport, isExtracted && style.extracted])} data-testid='panel-rundown'>
|
||||||
<div className={cx([style.rundownExport, isExtracted && style.extracted])} data-testid='panel-rundown'>
|
<FinderPlacement />
|
||||||
<FinderPlacement />
|
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
||||||
{isExtracted && <ViewNavigationMenu suppressSettings isNavigationLocked={getIsNavigationLocked()} />}
|
<div className={style.rundown}>
|
||||||
<div className={style.rundown}>
|
<Editor.Panel className={style.list}>
|
||||||
<Editor.Panel className={style.list}>
|
<ErrorBoundary>
|
||||||
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||||
|
<RundownRoot isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
|
<RundownContextMenu />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Editor.Panel>
|
||||||
|
{!hideSideBar && (
|
||||||
|
<div className={style.side}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
<RundownEntryEditor />
|
||||||
<RundownRoot isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
|
||||||
<RundownContextMenu />
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Editor.Panel>
|
</div>
|
||||||
{!hideSideBar && (
|
)}
|
||||||
<div className={style.side}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<RundownEntryEditor />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ProtectRoute>
|
</div>
|
||||||
</EntryActionsProvider>
|
</ProtectRoute>
|
||||||
</RundownSelectionContextProvider>
|
</EntryActionsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,16 +105,17 @@ interface RundownRootProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
|
function RundownRoot({ isSmallDevice, isExtracted, viewMode, setViewMode }: RundownRootProps) {
|
||||||
|
const source = useLoadedRundownSource();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.rundownRoot}>
|
<div className={style.rundownRoot}>
|
||||||
{isSmallDevice ? (
|
{isSmallDevice ? (
|
||||||
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeaderMobile viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
) : (
|
) : (
|
||||||
// TODO: add data-background-rundown={!isLoadedRundown} styling
|
|
||||||
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
<RundownHeader isExtracted={isExtracted} viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
)}
|
)}
|
||||||
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
{viewMode === RundownViewMode.List ? <RundownList /> : <RundownTable />}
|
||||||
{viewMode === RundownViewMode.Table && <EntryEditModal />}
|
{viewMode === RundownViewMode.Table && <EntryEditModal rundown={source.rundown} />}
|
||||||
<RenumberCuesDialog />
|
<RenumberCuesDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,34 @@
|
|||||||
import { Playback } from 'ontime-types';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import Empty from '../../common/components/state/Empty';
|
import EmptyFill from '../../common/components/state/EmptyFill';
|
||||||
import { useContextRundownList } from '../../common/hooks-query/useContextRundown';
|
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
import Rundown from './Rundown';
|
import Rundown from './Rundown';
|
||||||
|
|
||||||
const backgroundFeatureData = {
|
|
||||||
playback: Playback.Stop,
|
|
||||||
selectedEventId: null,
|
|
||||||
nextEventId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(RundownList);
|
export default memo(RundownList);
|
||||||
function RundownList() {
|
function RundownList() {
|
||||||
const { rundown, status, rundownMetadata, isLoadedRundown } = useContextRundownList();
|
const { data, status, isLoadingError, rundownMetadata } = useRundownWithMetadata();
|
||||||
const featureData = useRundownEditor();
|
const featureData = useRundownEditor();
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
const isLoading = status !== 'success' || !rundown || !rundownMetadata;
|
// avoid showing the editable empty state before we know whether the rundown is actually empty
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <EmptyFill text='Loading…' />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoadingError) {
|
||||||
return <Empty text='Connecting to server' />;
|
return <EmptyFill text={getLocalizedString('common.no_data')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Rundown
|
<Rundown
|
||||||
order={rundown.order}
|
order={data.order}
|
||||||
flatOrder={rundown.flatOrder}
|
flatOrder={data.flatOrder}
|
||||||
entries={rundown.entries}
|
entries={data.entries}
|
||||||
id={rundown.id}
|
id={data.id}
|
||||||
rundownMetadata={rundownMetadata}
|
rundownMetadata={rundownMetadata}
|
||||||
featureData={isLoadedRundown ? featureData : backgroundFeatureData}
|
featureData={featureData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.rundownSelect {
|
|
||||||
min-width: min(20rem, calc(100vw - 6rem));
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import Select from '../../../common/components/select/Select';
|
|
||||||
import { useRundownSelectionContext } from '../../../common/context/RundownSelectionContext';
|
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
|
||||||
|
|
||||||
import styles from './RundownSelect.module.scss';
|
|
||||||
|
|
||||||
const FOLLOW = '___null___';
|
|
||||||
|
|
||||||
interface RundownSelectProps {
|
|
||||||
appMode: AppMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RundownSelect({ appMode }: RundownSelectProps) {
|
|
||||||
'use memo';
|
|
||||||
const { selectRundownId, rundowns, loadedRundownId, selectedRundownId } = useRundownSelectionContext();
|
|
||||||
|
|
||||||
const options = rundowns.map(({ id, title }) => ({
|
|
||||||
value: id,
|
|
||||||
label: loadedRundownId === id ? `${title} (loaded)` : title,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// add a follow option
|
|
||||||
options.unshift({
|
|
||||||
value: FOLLOW,
|
|
||||||
label: 'Follow loaded',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.rundownSelect}>
|
|
||||||
<Select
|
|
||||||
value={selectedRundownId ?? FOLLOW}
|
|
||||||
options={options}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value === FOLLOW) selectRundownId(null);
|
|
||||||
else selectRundownId(value);
|
|
||||||
}}
|
|
||||||
disabled={appMode === AppMode.Run}
|
|
||||||
fluid
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OntimeEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { OntimeEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useRundown } from '../../../common/hooks-query/useRundown';
|
import useRundown from '../../../common/hooks-query/useRundown';
|
||||||
import { useEventSelection } from '../useEventSelection';
|
import { useEventSelection } from '../useEventSelection';
|
||||||
import EventEditorFooter from './composite/EventEditorFooter';
|
import EventEditorFooter from './composite/EventEditorFooter';
|
||||||
import EventEditor from './EventEditor';
|
import EventEditor from './EventEditor';
|
||||||
@@ -13,7 +13,7 @@ import style from './EntryEditor.module.scss';
|
|||||||
|
|
||||||
export default function RundownEntryEditor() {
|
export default function RundownEntryEditor() {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
const { data } = useRundown(null);
|
const { data } = useRundown();
|
||||||
|
|
||||||
const entry = useMemo<OntimeEntry | null>(() => {
|
const entry = useMemo<OntimeEntry | null>(() => {
|
||||||
if (data.order.length === 0) {
|
if (data.order.length === 0) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Button from '../../../common/components/buttons/Button';
|
|||||||
import Dialog from '../../../common/components/dialog/Dialog';
|
import Dialog from '../../../common/components/dialog/Dialog';
|
||||||
import Input from '../../../common/components/input/input/Input';
|
import Input from '../../../common/components/input/input/Input';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
import { useContextRundownCueRenumberModal } from '../../../common/hooks-query/useContextRundown';
|
import useRundown from '../../../common/hooks-query/useRundown';
|
||||||
import { orderEntries } from '../rundown.utils';
|
import { orderEntries } from '../rundown.utils';
|
||||||
import { useEventSelection } from '../useEventSelection';
|
import { useEventSelection } from '../useEventSelection';
|
||||||
|
|
||||||
@@ -17,7 +17,8 @@ type RenumberCueData = Pick<RenumberCues, 'increment' | 'prefix' | 'start'>;
|
|||||||
|
|
||||||
export default function RenumberCuesDialog() {
|
export default function RenumberCuesDialog() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const { flatOrder } = useContextRundownCueRenumberModal();
|
const { data } = useRundown();
|
||||||
|
const { flatOrder } = data;
|
||||||
const { onClose, isOpen } = useRenumberCuesDialogStore();
|
const { onClose, isOpen } = useRenumberCuesDialogStore();
|
||||||
const { renumberCues } = useEntryActionsContext();
|
const { renumberCues } = useEntryActionsContext();
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import Tooltip from '../../../common/components/tooltip/Tooltip';
|
|||||||
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
import { EditorLayoutMode, useEditorLayout } from '../../../views/editor/useEditorLayout';
|
||||||
import { RundownSelect } from '../common/RundownSelect';
|
|
||||||
import { RundownViewMode } from '../rundown.options';
|
import { RundownViewMode } from '../rundown.options';
|
||||||
import { useEditorFollowMode } from '../useEditorFollowMode';
|
import { useEditorFollowMode } from '../useEditorFollowMode';
|
||||||
import RundownMenu from './RundownMenu';
|
import RundownMenu from './RundownMenu';
|
||||||
@@ -25,7 +24,6 @@ interface HeaderControlsConfig {
|
|||||||
showRunEditToggle: boolean;
|
showRunEditToggle: boolean;
|
||||||
showOffsetToggle: boolean;
|
showOffsetToggle: boolean;
|
||||||
showOverflowMenu: boolean;
|
showOverflowMenu: boolean;
|
||||||
showRundownSelect: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConfig> = {
|
||||||
@@ -33,19 +31,16 @@ export const HEADER_CONTROLS_CONFIG: Record<EditorLayoutMode, HeaderControlsConf
|
|||||||
showRunEditToggle: true,
|
showRunEditToggle: true,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
showRundownSelect: false,
|
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.PLANNING]: {
|
[EditorLayoutMode.PLANNING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: false,
|
showOffsetToggle: false,
|
||||||
showOverflowMenu: true,
|
showOverflowMenu: true,
|
||||||
showRundownSelect: true,
|
|
||||||
},
|
},
|
||||||
[EditorLayoutMode.TRACKING]: {
|
[EditorLayoutMode.TRACKING]: {
|
||||||
showRunEditToggle: false,
|
showRunEditToggle: false,
|
||||||
showOffsetToggle: true,
|
showOffsetToggle: true,
|
||||||
showOverflowMenu: false,
|
showOverflowMenu: false,
|
||||||
showRundownSelect: false,
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -55,8 +50,7 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
const offsetMode = useOffsetMode();
|
const offsetMode = useOffsetMode();
|
||||||
const { layoutMode } = useEditorLayout();
|
const { layoutMode } = useEditorLayout();
|
||||||
|
|
||||||
const { showRunEditToggle, showOffsetToggle, showOverflowMenu, showRundownSelect } =
|
const { showRunEditToggle, showOffsetToggle, showOverflowMenu } = HEADER_CONTROLS_CONFIG[layoutMode];
|
||||||
HEADER_CONTROLS_CONFIG[layoutMode];
|
|
||||||
|
|
||||||
const toggleAppMode = (mode: AppMode[]) => {
|
const toggleAppMode = (mode: AppMode[]) => {
|
||||||
// we need to stop user from deselecting a mode
|
// we need to stop user from deselecting a mode
|
||||||
@@ -128,8 +122,6 @@ function RundownHeader({ isExtracted, viewMode, setViewMode }: RundownHeaderProp
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRundownSelect && <RundownSelect appMode={editorMode} />}
|
|
||||||
|
|
||||||
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
{showOverflowMenu && <RundownMenu allowNavigation={!isExtracted} />}
|
||||||
</Toolbar.Root>
|
</Toolbar.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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 useCustomFields from '../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||||
|
import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||||
|
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||||
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';
|
||||||
@@ -10,9 +12,11 @@ import { makeRundownColumns } from './makeRundownColumns';
|
|||||||
|
|
||||||
export default memo(RundownTable);
|
export default memo(RundownTable);
|
||||||
function RundownTable() {
|
function RundownTable() {
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
const { data: customFields } = useCustomFields();
|
||||||
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
|
||||||
const { editorMode } = useEditorFollowMode();
|
const { editorMode } = useEditorFollowMode();
|
||||||
|
const source = useLoadedRundownSource();
|
||||||
|
const actions = useEntryActions();
|
||||||
|
|
||||||
// Editor always has full permissions
|
// Editor always has full permissions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,15 +31,11 @@ function RundownTable() {
|
|||||||
|
|
||||||
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
||||||
|
|
||||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CuesheetDnd columns={columns} tableRoot='editor'>
|
<EntryActionsProvider actions={actions}>
|
||||||
{isLoading ? (
|
<CuesheetDnd columns={columns} tableRoot='editor'>
|
||||||
<EmptyPage text='Loading...' />
|
<CuesheetTable columns={columns} source={source} cuesheetMode={editorMode} tableRoot='editor' />
|
||||||
) : (
|
</CuesheetDnd>
|
||||||
<CuesheetTable columns={columns} cuesheetMode={editorMode} tableRoot='editor' />
|
</EntryActionsProvider>
|
||||||
)}
|
|
||||||
</CuesheetDnd>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function BackstageLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <Backstage {...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(null);
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ $dot-spacing: 1.5rem;
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background-color: var(--background-color-override, $viewer-background-color);
|
background-color: var(--background-color-override, $viewer-background-color);
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
@@ -21,26 +21,26 @@ $dot-spacing: 1.5rem;
|
|||||||
height: $dot-size;
|
height: $dot-size;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--accent-color-override, $ontime-color);
|
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) {
|
&:nth-child(1) {
|
||||||
left: $dot-size;
|
left: $dot-size;
|
||||||
animation: lds-ellipsis1 0.6s infinite;
|
animation: lds-ellipsis1 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
left: $dot-size;
|
left: $dot-size;
|
||||||
animation: lds-ellipsis2 0.6s infinite;
|
animation: lds-ellipsis2 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
left: calc($dot-size + $dot-spacing);
|
left: calc($dot-size + $dot-spacing);
|
||||||
animation: lds-ellipsis2 0.6s infinite;
|
animation: lds-ellipsis2 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(4) {
|
&:nth-child(4) {
|
||||||
left: calc($dot-size + 2 * $dot-spacing);
|
left: calc($dot-size + 2 * $dot-spacing);
|
||||||
animation: lds-ellipsis3 0.6s infinite;
|
animation: lds-ellipsis3 1s infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildre
|
|||||||
[filter],
|
[filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: events } = usePartialRundown(null, filterCallback);
|
const { data: events } = usePartialRundown(filterCallback);
|
||||||
|
|
||||||
const [firstIndex, setFirstIndex] = useState(-1);
|
const [firstIndex, setFirstIndex] = useState(-1);
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
|
|||||||
@@ -80,21 +80,7 @@ $item-height: 3.5rem;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-container {
|
.list-container {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function CountdownLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <Countdown {...data} />;
|
||||||
@@ -87,7 +87,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 +121,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 +137,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 +154,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(null);
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,7 @@
|
|||||||
'table';
|
'table';
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rundownSelect {
|
||||||
|
min-width: min(20rem, calc(100vw - 6rem));
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,46 +4,51 @@ import { IoApps } from 'react-icons/io5';
|
|||||||
import IconButton from '../../common/components/buttons/IconButton';
|
import IconButton from '../../common/components/buttons/IconButton';
|
||||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
||||||
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
import { useScopedRundown } from '../../common/hooks-query/useScopedRundown';
|
||||||
|
import { useScopedEntryActions } from '../../common/hooks/useEntryAction';
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import { getIsNavigationLocked } from '../../externals';
|
import { getIsNavigationLocked } from '../../externals';
|
||||||
import CuesheetOverview from '../../features/overview/CuesheetOverview';
|
import CuesheetOverview from '../../features/overview/CuesheetOverview';
|
||||||
import EntryEditModal from './cuesheet-edit-modal/EntryEditModal';
|
import EntryEditModal from './cuesheet-edit-modal/EntryEditModal';
|
||||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import CuesheetTableWrapper from './CuesheetTableWrapper';
|
import CuesheetTableWrapper from './CuesheetTableWrapper';
|
||||||
|
import { FOLLOW_LOADED_RUNDOWN_ID, useCuesheetRundownSelection } from './useCuesheetRundownSelection';
|
||||||
|
|
||||||
import styles from './CuesheetPage.module.scss';
|
import styles from './CuesheetPage.module.scss';
|
||||||
|
|
||||||
export default function CuesheetPage() {
|
export default function CuesheetPage() {
|
||||||
'use memo';
|
'use memo';
|
||||||
const [isMenuOpen, menuHandler] = useDisclosure();
|
const [isMenuOpen, menuHandler] = useDisclosure();
|
||||||
|
const { selectedRundownId, loadedRundownId, setSelectedRundownId, projectRundowns } = useCuesheetRundownSelection();
|
||||||
|
const source = useScopedRundown(selectedRundownId === FOLLOW_LOADED_RUNDOWN_ID ? loadedRundownId : selectedRundownId);
|
||||||
|
|
||||||
|
const actions = useScopedEntryActions(source.rundownId);
|
||||||
|
|
||||||
useWindowTitle('Cuesheet');
|
useWindowTitle('Cuesheet');
|
||||||
|
|
||||||
const isLocked = getIsNavigationLocked();
|
const isLocked = getIsNavigationLocked();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RundownSelectionContextProvider>
|
<EntryActionsProvider actions={actions}>
|
||||||
<EntryActionsProvider>
|
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
||||||
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
|
<EntryEditModal rundown={source.rundown} />
|
||||||
<EntryEditModal />
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<CuesheetOverview>
|
||||||
<CuesheetOverview>
|
{!isLocked && (
|
||||||
{!isLocked && (
|
<IconButton aria-label='Toggle navigation' variant='subtle-white' size='xlarge' onClick={menuHandler.open}>
|
||||||
<IconButton
|
<IoApps />
|
||||||
aria-label='Toggle navigation'
|
</IconButton>
|
||||||
variant='subtle-white'
|
)}
|
||||||
size='xlarge'
|
</CuesheetOverview>
|
||||||
onClick={menuHandler.open}
|
<CuesheetProgress />
|
||||||
>
|
<CuesheetTableWrapper
|
||||||
<IoApps />
|
source={source}
|
||||||
</IconButton>
|
selectedRundownId={selectedRundownId}
|
||||||
)}
|
loadedRundownId={loadedRundownId}
|
||||||
</CuesheetOverview>
|
setSelectedRundownId={setSelectedRundownId}
|
||||||
<CuesheetProgress />
|
projectRundowns={projectRundowns}
|
||||||
<CuesheetTableWrapper />
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EntryActionsProvider>
|
</EntryActionsProvider>
|
||||||
</RundownSelectionContextProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,108 @@
|
|||||||
|
import { MaybeString, ProjectRundown } from 'ontime-types';
|
||||||
import { memo, use, useMemo } from 'react';
|
import { memo, use, useMemo } from 'react';
|
||||||
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import Select from '../../common/components/select/Select';
|
||||||
import { PresetContext } from '../../common/context/PresetContext';
|
import { PresetContext } from '../../common/context/PresetContext';
|
||||||
import { useRundownSelectionContext } from '../../common/context/RundownSelectionContext';
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
|
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
|
||||||
|
import { AppMode } from '../../ontimeConfig';
|
||||||
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';
|
||||||
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
|
import { useApplyCuesheetPolicy } from './useApplyCuesheetPolicy';
|
||||||
|
import { FOLLOW_LOADED_RUNDOWN_ID } from './useCuesheetRundownSelection';
|
||||||
|
|
||||||
|
import styles from './CuesheetPage.module.scss';
|
||||||
|
|
||||||
|
interface CuesheetTableWrapperProps {
|
||||||
|
source: RundownSource;
|
||||||
|
selectedRundownId: MaybeString;
|
||||||
|
loadedRundownId: string;
|
||||||
|
setSelectedRundownId: (rundownId: string) => void;
|
||||||
|
projectRundowns: ProjectRundown[];
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(CuesheetTableWrapper);
|
export default memo(CuesheetTableWrapper);
|
||||||
function CuesheetTableWrapper() {
|
function CuesheetTableWrapper({
|
||||||
|
source,
|
||||||
|
selectedRundownId,
|
||||||
|
setSelectedRundownId,
|
||||||
|
loadedRundownId,
|
||||||
|
projectRundowns,
|
||||||
|
}: CuesheetTableWrapperProps) {
|
||||||
const preset = use(PresetContext);
|
const preset = use(PresetContext);
|
||||||
const { isLoadedRundown } = useRundownSelectionContext();
|
const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId;
|
||||||
|
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown });
|
||||||
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isLoadedRundown });
|
const { data: customFields } = useCustomFields();
|
||||||
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
|
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
|
||||||
[customFields, cuesheetMode, preset],
|
[customFields, cuesheetMode, preset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CuesheetDnd columns={columns}>
|
<CuesheetDnd columns={columns}>
|
||||||
{isLoading ? (
|
<CuesheetTable
|
||||||
<EmptyPage text='Loading...' />
|
columns={columns}
|
||||||
) : (
|
source={source}
|
||||||
<CuesheetTable
|
cuesheetMode={cuesheetMode}
|
||||||
columns={columns}
|
tableRoot='cuesheet'
|
||||||
cuesheetMode={cuesheetMode}
|
setCuesheetMode={setCuesheetMode}
|
||||||
tableRoot='cuesheet'
|
isCurrentRundown={isCurrentRundown}
|
||||||
setCuesheetMode={setCuesheetMode}
|
insertElement={
|
||||||
/>
|
<>
|
||||||
)}
|
<RundownSelect
|
||||||
|
cuesheetMode={cuesheetMode}
|
||||||
|
selectedRundownId={selectedRundownId}
|
||||||
|
loadedRundownId={loadedRundownId}
|
||||||
|
setSelectedRundownId={setSelectedRundownId}
|
||||||
|
projectRundowns={projectRundowns}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</CuesheetDnd>
|
</CuesheetDnd>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RundownSelectProps {
|
||||||
|
cuesheetMode: AppMode;
|
||||||
|
selectedRundownId: MaybeString;
|
||||||
|
loadedRundownId: string;
|
||||||
|
setSelectedRundownId: (rundownId: string) => void;
|
||||||
|
projectRundowns: ProjectRundown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RundownSelect({
|
||||||
|
cuesheetMode,
|
||||||
|
projectRundowns,
|
||||||
|
loadedRundownId,
|
||||||
|
selectedRundownId,
|
||||||
|
setSelectedRundownId,
|
||||||
|
}: RundownSelectProps) {
|
||||||
|
'use memo';
|
||||||
|
const options = projectRundowns.map(({ id, title }) => ({
|
||||||
|
value: id,
|
||||||
|
label: loadedRundownId === id ? `${title} (loaded)` : title,
|
||||||
|
}));
|
||||||
|
options.unshift({
|
||||||
|
value: FOLLOW_LOADED_RUNDOWN_ID,
|
||||||
|
label: 'Follow loaded', // TODO: Better wording and maybe icon? and translation
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.rundownSelect}>
|
||||||
|
<Select
|
||||||
|
value={selectedRundownId ?? undefined}
|
||||||
|
options={options}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) {
|
||||||
|
setSelectedRundownId(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={cuesheetMode === AppMode.Run}
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
FOLLOW_LOADED_RUNDOWN_ID,
|
||||||
|
getCuesheetRundownStorageKey,
|
||||||
|
resolveSelectedRundownId,
|
||||||
|
} from '../useCuesheetRundownSelection';
|
||||||
|
|
||||||
|
describe('useCuesheetRundownSelection helpers', () => {
|
||||||
|
it('builds a project-scoped storage key', () => {
|
||||||
|
expect(getCuesheetRundownStorageKey('http://localhost:4001', 'My Project')).toBe(
|
||||||
|
'cuesheet-selected-rundown:http://localhost:4001:My Project',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the follow loaded rundown when the stored selection is missing', () => {
|
||||||
|
expect(resolveSelectedRundownId('missing', new Set(['loaded', 'other']))).toBe(FOLLOW_LOADED_RUNDOWN_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the stored selection when it still exists in the current project', () => {
|
||||||
|
expect(resolveSelectedRundownId('other', new Set(['loaded', 'other']))).toBe('other');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import { Rundown } from 'ontime-types';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import Modal from '../../../common/components/modal/Modal';
|
import Modal from '../../../common/components/modal/Modal';
|
||||||
import { useContextRundownEditModal } from '../../../common/hooks-query/useContextRundown';
|
|
||||||
import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor';
|
import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor';
|
||||||
import { useEditModal } from './useEditModal';
|
import { useEditModal } from './useEditModal';
|
||||||
|
|
||||||
|
interface EntryEditModalProps {
|
||||||
|
rundown: Rundown;
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(EntryEditModal);
|
export default memo(EntryEditModal);
|
||||||
function EntryEditModal() {
|
function EntryEditModal({ rundown }: EntryEditModalProps) {
|
||||||
const { rundown } = useContextRundownEditModal();
|
|
||||||
const entryId = useEditModal((state) => state.selectedEntryId);
|
const entryId = useEditModal((state) => state.selectedEntryId);
|
||||||
const closeModal = useEditModal((state) => state.clearSelection);
|
const closeModal = useEditModal((state) => state.clearSelection);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ $table-header-font-size: calc(1rem - 2px);
|
|||||||
|
|
||||||
@include rows.cuesheet-row-columns($table-header-font-size);
|
@include rows.cuesheet-row-columns($table-header-font-size);
|
||||||
|
|
||||||
|
.tableLoading {
|
||||||
|
grid-area: table;
|
||||||
|
}
|
||||||
|
|
||||||
.cuesheet {
|
.cuesheet {
|
||||||
font-size: $table-font-size;
|
font-size: $table-font-size;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, Table, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { OntimeEntry, SupportedEntry, TimeField, isOntimeDelay, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { ComponentProps, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { ComponentProps, ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ContextProp,
|
ContextProp,
|
||||||
ItemProps,
|
ItemProps,
|
||||||
@@ -11,14 +11,15 @@ import {
|
|||||||
TableVirtuosoHandle,
|
TableVirtuosoHandle,
|
||||||
} from 'react-virtuoso';
|
} 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 EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
import { useContextRundownTable } from '../../../common/hooks-query/useContextRundown';
|
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
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 { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
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';
|
||||||
@@ -35,23 +36,36 @@ import style from './CuesheetTable.module.scss';
|
|||||||
type CuesheetTableBaseProps = {
|
type CuesheetTableBaseProps = {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
cuesheetMode: AppMode;
|
cuesheetMode: AppMode;
|
||||||
|
source: RundownSource;
|
||||||
|
insertElement?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
|
type EditorCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
tableRoot: 'editor';
|
tableRoot: 'editor';
|
||||||
setCuesheetMode?: undefined;
|
setCuesheetMode?: undefined;
|
||||||
|
isCurrentRundown?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
|
type ViewCuesheetTableProps = CuesheetTableBaseProps & {
|
||||||
tableRoot: 'cuesheet';
|
tableRoot: 'cuesheet';
|
||||||
setCuesheetMode: (mode: AppMode) => void;
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
|
isCurrentRundown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
|
type CuesheetTableProps = EditorCuesheetTableProps | ViewCuesheetTableProps;
|
||||||
|
|
||||||
export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCuesheetMode }: CuesheetTableProps) {
|
export default function CuesheetTable({
|
||||||
const { flatRundown, status, loadedEventId } = useContextRundownTable();
|
columns,
|
||||||
|
cuesheetMode,
|
||||||
|
source,
|
||||||
|
tableRoot,
|
||||||
|
setCuesheetMode,
|
||||||
|
isCurrentRundown,
|
||||||
|
insertElement,
|
||||||
|
}: CuesheetTableProps) {
|
||||||
|
const { flatRundown, status, isLoadingError, selectedEventId } = source;
|
||||||
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
|
||||||
|
const { getLocalizedString } = useTranslation();
|
||||||
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
|
||||||
|
|
||||||
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
||||||
@@ -131,17 +145,17 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCue
|
|||||||
|
|
||||||
// in Run mode, follow the current event
|
// in Run mode, follow the current event
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (virtuosoRef.current === null || cuesheetMode !== AppMode.Run || !loadedEventId) {
|
if (virtuosoRef.current === null || cuesheetMode !== AppMode.Run || !selectedEventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventIndex = flatRundown.findIndex((event) => event.id === loadedEventId);
|
const eventIndex = flatRundown.findIndex((event) => event.id === selectedEventId);
|
||||||
if (eventIndex === -1) {
|
if (eventIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'auto', align: 'start', offset: -50 });
|
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'auto', align: 'start', offset: -50 });
|
||||||
}, [cuesheetMode, flatRundown, loadedEventId]);
|
}, [cuesheetMode, flatRundown, selectedEventId]);
|
||||||
|
|
||||||
// Provide an imperative scroll handler for explicit jumps (finder/keyboard)
|
// Provide an imperative scroll handler for explicit jumps (finder/keyboard)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -216,10 +230,13 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCue
|
|||||||
});
|
});
|
||||||
}, [cuesheetMode, hideIndexColumn, table]);
|
}, [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) {
|
if (isLoadingError) {
|
||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -230,9 +247,17 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot, setCue
|
|||||||
handleResetResizing={resetColumnResizing}
|
handleResetResizing={resetColumnResizing}
|
||||||
handleResetReordering={resetColumnOrder}
|
handleResetReordering={resetColumnOrder}
|
||||||
handleClearToggles={setAllVisible}
|
handleClearToggles={setAllVisible}
|
||||||
appMode={cuesheetMode}
|
insertElement={insertElement}
|
||||||
tableRoot={tableRoot}
|
modeControls={
|
||||||
setCuesheetMode={setCuesheetMode}
|
tableRoot === 'cuesheet'
|
||||||
|
? {
|
||||||
|
cuesheetMode,
|
||||||
|
setCuesheetMode,
|
||||||
|
isCurrentRundown,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
showShare={tableRoot === 'cuesheet'}
|
||||||
/>
|
/>
|
||||||
<TableVirtuoso
|
<TableVirtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
|
|||||||
+27
-23
@@ -10,9 +10,7 @@ import Button from '../../../../common/components/buttons/Button';
|
|||||||
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
||||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
import PopoverContents from '../../../../common/components/popover/Popover';
|
import PopoverContents from '../../../../common/components/popover/Popover';
|
||||||
import { useRundownSelectionContext } from '../../../../common/context/RundownSelectionContext';
|
|
||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
import { RundownSelect } from '../../../../features/rundown/common/RundownSelect';
|
|
||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { useCuesheetPermissions } from '../../useTablePermissions';
|
import { useCuesheetPermissions } from '../../useTablePermissions';
|
||||||
import CuesheetShareModal from './CuesheetShareModal';
|
import CuesheetShareModal from './CuesheetShareModal';
|
||||||
@@ -32,15 +30,21 @@ type TableHeaderOptionValues = Pick<
|
|||||||
'hideTableSeconds' | 'hideIndexColumn' | 'showDelayedTimes' | 'hideDelays'
|
'hideTableSeconds' | 'hideIndexColumn' | 'showDelayedTimes' | 'hideDelays'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type TableModeControls = {
|
||||||
|
cuesheetMode: AppMode;
|
||||||
|
setCuesheetMode: (mode: AppMode) => void;
|
||||||
|
isCurrentRundown?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
interface CuesheetTableHeaderToolbarProps {
|
interface CuesheetTableHeaderToolbarProps {
|
||||||
columns: Column<ExtendedEntry, unknown>[];
|
columns: Column<ExtendedEntry, unknown>[];
|
||||||
optionsStore: TableHeaderOptionsStore;
|
optionsStore: TableHeaderOptionsStore;
|
||||||
handleResetResizing: () => void;
|
handleResetResizing: () => void;
|
||||||
handleResetReordering: () => void;
|
handleResetReordering: () => void;
|
||||||
handleClearToggles: () => void;
|
handleClearToggles: () => void;
|
||||||
setCuesheetMode?: (mode: AppMode) => void;
|
insertElement?: ReactNode;
|
||||||
appMode: AppMode;
|
modeControls?: TableModeControls;
|
||||||
tableRoot: 'editor' | 'cuesheet';
|
showShare?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetTableHeaderToolbar({
|
export default function CuesheetTableHeaderToolbar({
|
||||||
@@ -49,23 +53,23 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
handleResetResizing,
|
handleResetResizing,
|
||||||
handleResetReordering,
|
handleResetReordering,
|
||||||
handleClearToggles,
|
handleClearToggles,
|
||||||
setCuesheetMode,
|
insertElement,
|
||||||
tableRoot,
|
modeControls,
|
||||||
appMode,
|
showShare = false,
|
||||||
}: CuesheetTableHeaderToolbarProps) {
|
}: CuesheetTableHeaderToolbarProps) {
|
||||||
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode) && tableRoot === 'cuesheet';
|
const canChangeMode = useCuesheetPermissions((state) => state.canChangeMode);
|
||||||
const canShare = useCuesheetPermissions((state) => state.canShare) && tableRoot === 'cuesheet';
|
const canShare = useCuesheetPermissions((state) => state.canShare);
|
||||||
const showRundownSelect = tableRoot === 'cuesheet';
|
|
||||||
const { isLoadedRundown } = useRundownSelectionContext();
|
|
||||||
|
|
||||||
const toggleCuesheetMode = (mode: AppMode[]) => {
|
const toggleCuesheetMode = (mode: AppMode[]) => {
|
||||||
const newValue = mode.at(0);
|
const newValue = mode.at(0);
|
||||||
if (!newValue || !setCuesheetMode) return;
|
if (!newValue || !modeControls) return;
|
||||||
setCuesheetMode(newValue);
|
modeControls.setCuesheetMode(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBackground = !(modeControls?.isCurrentRundown ?? true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar.Root className={style.tableSettings} data-background-rundown={!isLoadedRundown}>
|
<Toolbar.Root className={style.tableSettings} data-background-rundown={isBackground}>
|
||||||
<ViewSettings optionsStore={optionsStore} />
|
<ViewSettings optionsStore={optionsStore} />
|
||||||
<ColumnSettings
|
<ColumnSettings
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -73,14 +77,14 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
handleResetReordering={handleResetReordering}
|
handleResetReordering={handleResetReordering}
|
||||||
handleClearToggles={handleClearToggles}
|
handleClearToggles={handleClearToggles}
|
||||||
/>
|
/>
|
||||||
<div className={style.apart}>
|
{modeControls && canChangeMode && (
|
||||||
{showRundownSelect && <RundownSelect appMode={appMode} />}
|
<div className={style.apart}>
|
||||||
{canChangeMode && (
|
{insertElement}
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={[appMode]}
|
value={[modeControls.cuesheetMode]}
|
||||||
onValueChange={toggleCuesheetMode}
|
onValueChange={toggleCuesheetMode}
|
||||||
className={style.group}
|
className={style.group}
|
||||||
disabled={!isLoadedRundown}
|
disabled={!modeControls.isCurrentRundown}
|
||||||
>
|
>
|
||||||
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
||||||
Run
|
Run
|
||||||
@@ -89,10 +93,10 @@ export default function CuesheetTableHeaderToolbar({
|
|||||||
Edit
|
Edit
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{canShare && (
|
{showShare && canShare && (
|
||||||
<>
|
<>
|
||||||
<Editor.Separator orientation='vertical' />
|
<Editor.Separator orientation='vertical' />
|
||||||
<CuesheetShareModal />
|
<CuesheetShareModal />
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
import { startTransition, useCallback, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { useOrderedProjectList } from '../../common/hooks-query/useProjectList';
|
||||||
|
import { useProjectRundowns } from '../../common/hooks-query/useProjectRundowns';
|
||||||
|
import { serverURL } from '../../externals';
|
||||||
|
|
||||||
|
export const FOLLOW_LOADED_RUNDOWN_ID = '__follow-loaded__' as const;
|
||||||
|
|
||||||
|
export function getCuesheetRundownStorageKey(server: string, projectFilename: string) {
|
||||||
|
return `cuesheet-selected-rundown:${server}:${projectFilename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSelectedRundownId(storedSelectedRundownId: string | null, availableRundownIds: Set<string>) {
|
||||||
|
if (storedSelectedRundownId && availableRundownIds.has(storedSelectedRundownId)) return storedSelectedRundownId;
|
||||||
|
return FOLLOW_LOADED_RUNDOWN_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCuesheetRundownSelection() {
|
||||||
|
'use memo';
|
||||||
|
|
||||||
|
const { data: projectRundowns } = useProjectRundowns();
|
||||||
|
const {
|
||||||
|
data: { lastLoadedProject },
|
||||||
|
} = useOrderedProjectList();
|
||||||
|
const storageKey = useMemo(() => getCuesheetRundownStorageKey(serverURL, lastLoadedProject), [lastLoadedProject]);
|
||||||
|
const [storedSelectedRundownId, setStoredSelectedRundownId] = useSessionStorage<string | null>({
|
||||||
|
key: storageKey,
|
||||||
|
defaultValue: FOLLOW_LOADED_RUNDOWN_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableRundownIds = new Set(projectRundowns.rundowns.map(({ id }) => id)).add(FOLLOW_LOADED_RUNDOWN_ID);
|
||||||
|
const { loaded: loadedRundownId } = projectRundowns;
|
||||||
|
|
||||||
|
const selectedRundownId = resolveSelectedRundownId(storedSelectedRundownId, availableRundownIds);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadedRundownId,
|
||||||
|
selectedRundownId,
|
||||||
|
projectRundowns: projectRundowns.rundowns,
|
||||||
|
setSelectedRundownId: (rundownId: string) => {
|
||||||
|
startTransition(() => {
|
||||||
|
setStoredSelectedRundownId(rundownId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDirectLinkToBackgroundEdit() {
|
||||||
|
const {
|
||||||
|
data: { lastLoadedProject },
|
||||||
|
} = useOrderedProjectList();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const storageKey = getCuesheetRundownStorageKey(serverURL, lastLoadedProject);
|
||||||
|
const [_, setStoredSelectedRundownId] = useSessionStorage<string | null>({ key: storageKey, defaultValue: null });
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (rundownId: string) => {
|
||||||
|
await navigate('/cuesheet');
|
||||||
|
startTransition(() => setStoredSelectedRundownId(rundownId));
|
||||||
|
},
|
||||||
|
[setStoredSelectedRundownId, navigate],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
import { RundownSelectionContextProvider } from '../../common/context/RundownSelectionContext';
|
|
||||||
import TrackingPlaybackBar from '../../features/control/playback/tracking-playback-bar/TrackingPlaybackBar';
|
import TrackingPlaybackBar from '../../features/control/playback/tracking-playback-bar/TrackingPlaybackBar';
|
||||||
import { AppMode } from '../../ontimeConfig';
|
import { AppMode } from '../../ontimeConfig';
|
||||||
import TitleList from './title-list/TitleList';
|
import TitleList from './title-list/TitleList';
|
||||||
@@ -15,51 +14,44 @@ const MessageControl = lazy(() => import('../../features/control/message/Message
|
|||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { layoutMode } = useEditorLayout();
|
const { layoutMode } = useEditorLayout();
|
||||||
|
|
||||||
switch (layoutMode) {
|
if (layoutMode === EditorLayoutMode.CONTROL) {
|
||||||
case EditorLayoutMode.TRACKING: {
|
return (
|
||||||
return (
|
<div id='panels' className={styles.panelContainer}>
|
||||||
<div id='panels' className={`${styles.panelContainer} ${styles.panelContainerTracking}`}>
|
<div className={styles.left}>
|
||||||
<div className={styles.rundownLayout}>
|
<TimerControl />
|
||||||
<div className={styles.titlesPanel}>
|
<MessageControl />
|
||||||
<RundownSelectionContextProvider>
|
|
||||||
<TitleList mode={AppMode.Run} />
|
|
||||||
</RundownSelectionContextProvider>
|
|
||||||
</div>
|
|
||||||
<div className={styles.rundownPanel}>
|
|
||||||
<Rundown />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TrackingPlaybackBar />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Rundown />
|
||||||
}
|
</div>
|
||||||
case EditorLayoutMode.PLANNING: {
|
);
|
||||||
return (
|
}
|
||||||
<div id='panels' className={styles.panelContainer}>
|
|
||||||
<div className={styles.rundownLayout}>
|
if (layoutMode === EditorLayoutMode.TRACKING) {
|
||||||
<div className={styles.titlesPanel}>
|
return (
|
||||||
<RundownSelectionContextProvider>
|
<div id='panels' className={`${styles.panelContainer} ${styles.panelContainerTracking}`}>
|
||||||
<TitleList mode={AppMode.Edit} />
|
<div className={styles.rundownLayout}>
|
||||||
</RundownSelectionContextProvider>
|
<div className={styles.titlesPanel}>
|
||||||
</div>
|
<TitleList mode={AppMode.Run} />
|
||||||
<div className={styles.rundownPanel}>
|
</div>
|
||||||
<Rundown />
|
<div className={styles.rundownPanel}>
|
||||||
</div>
|
<Rundown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<TrackingPlaybackBar />
|
||||||
}
|
</div>
|
||||||
case EditorLayoutMode.CONTROL:
|
);
|
||||||
default: {
|
}
|
||||||
return (
|
|
||||||
<div id='panels' className={styles.panelContainer}>
|
return (
|
||||||
<div className={styles.left}>
|
<div id='panels' className={styles.panelContainer}>
|
||||||
<TimerControl />
|
<div className={styles.rundownLayout}>
|
||||||
<MessageControl />
|
<div className={styles.titlesPanel}>
|
||||||
</div>
|
<TitleList mode={AppMode.Edit} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.rundownPanel}>
|
||||||
<Rundown />
|
<Rundown />
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EntryId, MaybeString, SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
import { EntryId, MaybeString, SupportedEntry, isOntimeEvent, isOntimeGroup, isOntimeMilestone } from 'ontime-types';
|
||||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useContextRundownFinder } from '../../../common/hooks-query/useContextRundown';
|
import { useFlatRundown } from '../../../common/hooks-query/useRundown';
|
||||||
import { useSelectAndRevealEntry } from '../../../features/rundown/useSelectAndRevealEntry';
|
import { useSelectAndRevealEntry } from '../../../features/rundown/useSelectAndRevealEntry';
|
||||||
|
|
||||||
const maxResults = 12;
|
const maxResults = 12;
|
||||||
@@ -38,7 +38,7 @@ type FilterableMilestone = {
|
|||||||
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
|
type FilterableEntry = FilterableGroup | FilterableEvent | FilterableMilestone;
|
||||||
|
|
||||||
export default function useFinder() {
|
export default function useFinder() {
|
||||||
const { rundown: data, rundownId } = useContextRundownFinder();
|
const { data, rundownId } = useFlatRundown();
|
||||||
const [results, setResults] = useState<FilterableEntry[]>([]);
|
const [results, setResults] = useState<FilterableEntry[]>([]);
|
||||||
const [error, setError] = useState<MaybeString>(null);
|
const [error, setError] = useState<MaybeString>(null);
|
||||||
const lastSearchString = useRef('');
|
const lastSearchString = useRef('');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
import ScrollArea from '../../../common/components/scroll-area/ScrollArea';
|
import ScrollArea from '../../../common/components/scroll-area/ScrollArea';
|
||||||
import { useContextRundownList } from '../../../common/hooks-query/useContextRundown';
|
import useRundown from '../../../common/hooks-query/useRundown';
|
||||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
||||||
import { ExtendedEntry, getFlatRundownMetadata } from '../../../common/utils/rundownMetadata';
|
import { ExtendedEntry, getFlatRundownMetadata } from '../../../common/utils/rundownMetadata';
|
||||||
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
import { useEventSelection } from '../../../features/rundown/useEventSelection';
|
||||||
@@ -20,7 +20,7 @@ interface TitleListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleList({ mode }: TitleListProps) {
|
export default function TitleList({ mode }: TitleListProps) {
|
||||||
const { rundown } = useContextRundownList();
|
const { data: rundown } = useRundown();
|
||||||
const selectedEventId = useSelectedEventId();
|
const selectedEventId = useSelectedEventId();
|
||||||
const cursor = useEventSelection((state) => state.cursor);
|
const cursor = useEventSelection((state) => state.cursor);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { isValueOfEnum } from 'ontime-utils';
|
import { isValueOfEnum } from 'ontime-utils';
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { setSelectRundownInParams } from '../../common/context/RundownSelectionContext';
|
|
||||||
|
|
||||||
const layoutParam = 'layout';
|
const layoutParam = 'layout';
|
||||||
|
|
||||||
export enum EditorLayoutMode {
|
export enum EditorLayoutMode {
|
||||||
@@ -23,29 +20,14 @@ function getEditorLayout(value: string | null): EditorLayoutMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useEditorLayout() {
|
export function useEditorLayout() {
|
||||||
'use memo';
|
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
const layoutMode = getEditorLayout(searchParams.get(layoutParam));
|
||||||
|
|
||||||
useEffect(() => {
|
const setLayoutMode = (mode: EditorLayoutMode) => {
|
||||||
setSearchParams((searchParams) => {
|
const nextParams = new URLSearchParams(searchParams);
|
||||||
if (layoutMode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams);
|
nextParams.set(layoutParam, mode);
|
||||||
return searchParams;
|
setSearchParams(nextParams, { replace: true });
|
||||||
});
|
};
|
||||||
}, [setSearchParams, layoutMode]);
|
|
||||||
|
|
||||||
const setLayoutMode = useCallback(
|
|
||||||
(mode: EditorLayoutMode) => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.set(layoutParam, mode);
|
|
||||||
// Only the Planning layout is allowed to look at something other than the current rundown
|
|
||||||
if (mode !== EditorLayoutMode.PLANNING) setSelectRundownInParams(null, searchParams);
|
|
||||||
return searchParams;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setSearchParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { layoutMode, setLayoutMode };
|
return { layoutMode, setLayoutMode };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@use '@/theme/viewerDefs' as *;
|
@use '@/theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
$content-width: min(100%, 1100px);
|
||||||
|
|
||||||
.project {
|
.project {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-sizing: border-box; /* reset */
|
box-sizing: border-box; /* reset */
|
||||||
@@ -16,56 +18,104 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* =================== HEADER ===================*/
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
width: $content-width;
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(12px, 2vw, 24px);
|
||||||
|
|
||||||
|
padding-bottom: $view-element-gap;
|
||||||
|
border-bottom: 1px solid $white-10;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
max-width: min(200px, 30vw);
|
max-width: min(200px, 30vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: $header-font-size;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: $base-font-size;
|
||||||
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =================== CONTENT ===================*/
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 100%;
|
width: $content-width;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: min(calc(100vw - 4rem), 960px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: start;
|
|
||||||
gap: $view-element-gap;
|
gap: $view-element-gap;
|
||||||
|
|
||||||
|
padding-block: $view-element-gap;
|
||||||
padding-bottom: 10vh;
|
padding-bottom: 10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info__card {
|
||||||
|
background-color: var(--card-background-color-override, $viewer-card-bg-color);
|
||||||
|
border-radius: $element-border-radius;
|
||||||
|
padding: $view-block-padding $view-inline-padding;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__media {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $view-element-gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info__media .info__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.info__label {
|
.info__label {
|
||||||
|
font-size: $timer-label-size;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
color: var(--label-color-override, $viewer-label-color);
|
color: var(--label-color-override, $viewer-label-color);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info__value {
|
.info__value {
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
}
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
.info__custom {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info__image-container {
|
.info__image-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 192px;
|
flex: 0 0 min(192px, 25%);
|
||||||
height: 192px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info__image {
|
.info__image {
|
||||||
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
height: auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link.info__value {
|
.link.info__value {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
gap: $view-element-gap;
|
gap: 0.35em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $action-text-color;
|
color: $action-text-color;
|
||||||
|
|
||||||
@@ -78,12 +128,13 @@
|
|||||||
/* =================== MOBILE ===================*/
|
/* =================== MOBILE ===================*/
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.project {
|
.project {
|
||||||
|
.project-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
.logo img {
|
.logo img {
|
||||||
height: min(50px, 10vh);
|
height: min(50px, 10vh);
|
||||||
}
|
}
|
||||||
.info__image-container {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OntimeView } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
|
import { type ReactNode, useState } from 'react';
|
||||||
import { IoOpenOutline } from 'react-icons/io5';
|
import { IoOpenOutline } from 'react-icons/io5';
|
||||||
|
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
@@ -21,7 +22,7 @@ export default function ProjectInfoLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <ProjectInfo {...data} />;
|
||||||
@@ -41,56 +42,49 @@ 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')} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasHeader = Boolean(projectData.logo || projectData.title || projectData.description);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'>
|
<div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'>
|
||||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||||
{projectData.logo && <ViewLogo name={projectData.logo} className='logo' />}
|
{hasHeader && (
|
||||||
|
<div className='project-header'>
|
||||||
|
{projectData.logo && <ViewLogo name={projectData.logo} className='logo' />}
|
||||||
|
<div className='project-header__text'>
|
||||||
|
{projectData.title && <div className='title'>{projectData.title}</div>}
|
||||||
|
{projectData.description && <div className='description'>{projectData.description}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
{projectData.title && (
|
{projectData.info && <InfoCard label={getLocalizedString('project.info')}>{projectData.info}</InfoCard>}
|
||||||
<div>
|
|
||||||
<div className='info__label'>{getLocalizedString('project.title')}</div>
|
|
||||||
<div className='info__value'>{projectData.title}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{projectData.description && (
|
|
||||||
<div>
|
|
||||||
<div className='info__label'>{getLocalizedString('project.description')}</div>
|
|
||||||
<div className='info__value'>{projectData.description}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{projectData.info && (
|
|
||||||
<div>
|
|
||||||
<div className='info__label'>{getLocalizedString('project.info')}</div>
|
|
||||||
<div className='info__value'>{projectData.info}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{projectData.url && (
|
{projectData.url && (
|
||||||
<div>
|
<div className='info__card'>
|
||||||
<div className='info__label'>{getLocalizedString('project.url')}</div>
|
<div className='info__label'>{getLocalizedString('project.url')}</div>
|
||||||
<a href={projectData.url} target='_blank' rel='noreferrer' className='info__value link'>
|
<a href={projectData.url} target='_blank' rel='noreferrer' className='info__value link'>
|
||||||
{projectData.url} <IoOpenOutline style={{ fontSize: '1em' }} />
|
{projectData.url}
|
||||||
|
<IoOpenOutline style={{ fontSize: '1em' }} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{projectData.custom.map((info, idx) => {
|
{projectData.custom.map((info, idx) => {
|
||||||
const hasUrl = Boolean(info.url);
|
|
||||||
return (
|
return (
|
||||||
// oxlint-disable-next-line react/no-array-index-key - we only have the index to go of here
|
// 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__custom'>
|
<div key={`${info.title}-${idx}`} className='info__card'>
|
||||||
{hasUrl && (
|
{info.title && <div className='info__label'>{info.title}</div>}
|
||||||
<div className='info__image-container'>
|
{info.url ? (
|
||||||
<img className='info__image' src={info.url} loading='lazy' />
|
<div className='info__media'>
|
||||||
|
<InfoImage src={info.url} />
|
||||||
|
{info.value && <div className='info__value'>{info.value}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
info.value && <div className='info__value'>{info.value}</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
|
||||||
<div className='info__label'>{info.title}</div>
|
|
||||||
<div className='info__value'>{info.value}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -98,3 +92,31 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
label: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoCard({ label, children }: InfoCardProps) {
|
||||||
|
return (
|
||||||
|
<div className='info__card'>
|
||||||
|
<div className='info__label'>{label}</div>
|
||||||
|
<div className='info__value'>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoImage({ src }: { src: string }) {
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='info__image-container'>
|
||||||
|
<img className='info__image' src={src} loading='lazy' alt='' onError={() => setHasError(true)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 }]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function StudioLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <Studio {...data} />;
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { OntimeView } from 'ontime-types';
|
import { OntimeView } from 'ontime-types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import EmptyFill from '../../common/components/state/EmptyFill';
|
||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
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';
|
||||||
@@ -29,7 +30,7 @@ export default function TimelinePageLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <TimelinePage {...data} />;
|
||||||
@@ -73,7 +74,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
|
|||||||
totalDuration={totalDuration}
|
totalDuration={totalDuration}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyPage text={getLocalizedString('common.no_data')} />
|
<EmptyFill text={getLocalizedString('common.no_data')} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(null);
|
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 },
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function TimerLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'error') {
|
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} />;
|
return <Timer {...data} />;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CustomFields, ProjectData, RundownEntries, Settings, ViewSettings } fro
|
|||||||
|
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import { useRundown } from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
@@ -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(null);
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { copyFile } from 'fs/promises';
|
import { copyFile } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { DatabaseModel, LogOrigin, ProjectFileListResponse, RefetchKey } from 'ontime-types';
|
import { DatabaseModel, LogOrigin, ProjectFileListResponse } from 'ontime-types';
|
||||||
import { getErrorMessage, getFirstRundown } from 'ontime-utils';
|
import { getErrorMessage, getFirstRundown } from 'ontime-utils';
|
||||||
|
|
||||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
|
||||||
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
|
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
|
||||||
import { parseDatabaseModel } from '../../api-data/db/db.parser.js';
|
import { parseDatabaseModel } from '../../api-data/db/db.parser.js';
|
||||||
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
|
import { getCurrentRundown } from '../../api-data/rundown/rundown.dao.js';
|
||||||
@@ -106,9 +105,6 @@ async function loadProject(projectData: DatabaseModel, fileName: string, rundown
|
|||||||
currentProjectName: fileName,
|
currentProjectName: fileName,
|
||||||
};
|
};
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
sendRefetch(RefetchKey.ProjectFiles);
|
|
||||||
});
|
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,10 +263,6 @@ export async function duplicateProjectFile(originalFile: string, newFilename: st
|
|||||||
|
|
||||||
const pathToDuplicate = getPathToProject(newFilename);
|
const pathToDuplicate = getPathToProject(newFilename);
|
||||||
await copyFile(projectFilePath, pathToDuplicate);
|
await copyFile(projectFilePath, pathToDuplicate);
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
sendRefetch(RefetchKey.ProjectFiles);
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,10 +293,6 @@ export async function renameProjectFile(originalFile: string, newFilename: strin
|
|||||||
const newFileName = await loadProject(projectData.data, newFilename);
|
const newFileName = await loadProject(projectData.data, newFilename);
|
||||||
return newFileName;
|
return newFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
setImmediate(() => {
|
|
||||||
sendRefetch(RefetchKey.ProjectFiles);
|
|
||||||
});
|
|
||||||
return newFilename;
|
return newFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,9 +332,6 @@ export async function deleteProjectFile(filename: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await deleteFile(projectFilePath);
|
await deleteFile(projectFilePath);
|
||||||
setImmediate(() => {
|
|
||||||
sendRefetch(RefetchKey.ProjectFiles);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -389,7 +374,6 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedData = getDataProvider().getData();
|
const updatedData = await getDataProvider().getData();
|
||||||
|
|
||||||
return updatedData;
|
return updatedData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export enum RefetchKey {
|
export enum RefetchKey {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
CustomFields = 'custom-fields',
|
CustomFields = 'custom-fields',
|
||||||
ProjectFiles = 'project-files',
|
|
||||||
ProjectData = 'project-data',
|
ProjectData = 'project-data',
|
||||||
ProjectRundowns = 'project-rundowns',
|
ProjectRundowns = 'project-rundowns',
|
||||||
Report = 'report',
|
Report = 'report',
|
||||||
|
|||||||
Reference in New Issue
Block a user