Remove public event feature (#1645)

This commit is contained in:
Alex Christoffer Rasmussen
2025-06-19 18:07:42 +02:00
committed by Carlos Valente
parent 0649678dca
commit 08d9e24871
99 changed files with 173 additions and 1129 deletions
-7
View File
@@ -103,13 +103,6 @@ IP.ADDRESS:4001/studio > Studio Clock
IP.ADDRESS:4001/timeline > Timeline
```
```
For the public views
-------------------------------------------------------------
IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/lower > Lower Thirds
```
```
For production views
-------------------------------------------------------------
-10
View File
@@ -29,7 +29,6 @@ const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdow
const Backstage = React.lazy(() => import('./views/backstage/Backstage'));
const Timeline = React.lazy(() => import('./views/timeline/TimelinePage'));
const Public = React.lazy(() => import('./views/public/Public'));
const Lower = React.lazy(() => import('./features/viewers/lower-thirds/LowerThird'));
const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClock'));
const ProjectInfo = React.lazy(() => import('./views/project-info/ProjectInfo'));
@@ -40,7 +39,6 @@ const SClock = withPreset(withData(ClockView));
const SCountdown = withPreset(withData(Countdown));
const SBackstage = withPreset(withData(Backstage));
const SProjectInfo = withPreset(withData(ProjectInfo));
const SPublic = withPreset(withData(Public));
const SLowerThird = withPreset(withData(Lower));
const SStudio = withPreset(withData(StudioClock));
const STimeline = withPreset(withData(Timeline));
@@ -88,14 +86,6 @@ export default function AppRouter() {
</ViewLoader>
}
/>
<Route
path='/public'
element={
<ViewLoader>
<SPublic />
</ViewLoader>
}
/>
<Route
path='/minimal'
element={
@@ -36,11 +36,10 @@ import { logAxiosError } from '../api/utils';
import { useEditorSettings } from '../stores/editorSettings';
export type EventOptions = Partial<{
// options to any new block (event / delay / block)
// options of any new entries (event / delay / block)
after: MaybeString;
before: MaybeString;
// options to blocks of type OntimeEvent
defaultPublic: boolean;
// options of entries of type OntimeEvent
linkPrevious: boolean;
lastEventId: MaybeString;
}>;
@@ -51,7 +50,6 @@ export type EventOptions = Partial<{
export const useEntryActions = () => {
const queryClient = useQueryClient();
const {
defaultPublic,
linkPrevious,
defaultTimeStrategy,
defaultDuration,
@@ -95,7 +93,6 @@ export const useEntryActions = () => {
const applicationOptions = {
after: options?.after,
before: options?.before,
defaultPublic: options?.defaultPublic ?? defaultPublic,
lastEventId: options?.lastEventId,
linkPrevious: options?.linkPrevious ?? linkPrevious,
};
@@ -111,7 +108,6 @@ export const useEntryActions = () => {
// Override event with options from editor settings
newEntry.linkStart = applicationOptions.linkPrevious;
newEntry.isPublic = applicationOptions.defaultPublic;
if (newEntry.duration === undefined && newEntry.timeEnd === undefined) {
newEntry.duration = parseUserTime(defaultDuration);
@@ -157,7 +153,6 @@ export const useEntryActions = () => {
defaultDangerTime,
defaultDuration,
defaultEndAction,
defaultPublic,
defaultTimerType,
defaultTimeStrategy,
defaultWarnTime,
@@ -1,46 +0,0 @@
import { useEffect, useState } from 'react';
const scriptTagId = 'ontime-override';
export const useRuntimeStylesheet = (pathToFile) => {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(pathToFile);
if (response.ok) {
return response.text();
}
};
if (!pathToFile) {
document.getElementById(scriptTagId)?.remove();
setShouldRender(true);
return;
}
if (document.getElementById(scriptTagId)) {
setShouldRender(true);
return;
}
setShouldRender(false);
const styleSheet = document.createElement('style');
styleSheet.rel = 'stylesheet';
styleSheet.setAttribute('id', scriptTagId);
fetchData()
.then((data) => {
styleSheet.innerHTML = data;
document.head.append(styleSheet);
})
.catch((error) => {
console.error(`Error loading stylesheet: ${error}`);
})
.finally(() => {
// schedule render for next tick
setTimeout(() => setShouldRender(true), 0);
});
}, [pathToFile]);
return { shouldRender };
};
@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
const scriptTagId = 'ontime-override';
export const useRuntimeStylesheet = (pathToFile?: string): { shouldRender: boolean } => {
const [shouldRender, setShouldRender] = useState(false);
/**
* When a view mounts or the stylesheet path changes we need to handle potentially loading a new stylesheet
* - if no path is given, ensure there is no stylesheet loaded
* - if a path is given, fetch the stylesheet and inject it into the document head
* @returns { shouldRender: boolean } - after the stylesheet is handled and the clients are ready to render
*/
useEffect(() => {
if (!pathToFile) {
handleNoStylesheet();
return;
}
// there is already a stylesheet loaded, nothing further to do
if (document.getElementById(scriptTagId)) {
setShouldRender(true);
return;
}
setShouldRender(false);
fetchStylesheetData(pathToFile)
.then((data: string | undefined) => {
if (!data) {
console.error('Error loading stylesheet: no data');
return;
}
return injectStylesheet(data);
})
.catch((error: unknown) => {
console.error(`Error loading stylesheet: ${error}`);
})
.finally(() => {
// schedule render for next tick
setTimeout(() => setShouldRender(true), 0);
});
/**
* No stylesheet was provided, remove any existing stylesheet
*/
function handleNoStylesheet() {
document.getElementById(scriptTagId)?.remove();
setShouldRender(true);
}
/**
* Get data from backend
*/
async function fetchStylesheetData(path: string) {
const response = await fetch(path);
if (response.ok) {
return response.text();
}
return undefined;
}
/**
* Add a stylesheet with given content to the document head
*/
async function injectStylesheet(styleContent: string) {
const styleSheet = document.createElement('style');
styleSheet.setAttribute('id', scriptTagId);
styleSheet.innerHTML = styleContent;
document.head.append(styleSheet);
}
}, [pathToFile]);
return { shouldRender };
};
@@ -3,8 +3,6 @@ import { ProjectData } from 'ontime-types';
export const projectDataPlaceholder: ProjectData = {
title: '',
description: '',
publicUrl: '',
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
projectLogo: null,
@@ -10,7 +10,6 @@ type EditorSettingsStore = {
defaultTimeStrategy: TimeStrategy;
defaultWarnTime: string;
defaultDangerTime: string;
defaultPublic: boolean;
defaultTimerType: TimerType;
defaultEndAction: EndAction;
setDefaultDuration: (defaultDuration: string) => void;
@@ -18,7 +17,6 @@ type EditorSettingsStore = {
setTimeStrategy: (timeStrategy: TimeStrategy) => void;
setWarnTime: (warnTime: string) => void;
setDangerTime: (dangerTime: string) => void;
setDefaultPublic: (defaultPublic: boolean) => void;
setDefaultTimerType: (defaultTimerType: TimerType) => void;
setDefaultEndAction: (defaultEndAction: EndAction) => void;
};
@@ -29,7 +27,6 @@ export const editorSettingsDefaults = {
timeStrategy: TimeStrategy.LockDuration,
warnTime: '00:02:00', // 120000 same as backend
dangerTime: '00:01:00', // 60000 same as backend
isPublic: true,
timerType: TimerType.CountDown,
endAction: EndAction.None,
};
@@ -40,7 +37,6 @@ enum EditorSettingsKeys {
DefaultTimeStrategy = 'ontime-time-strategy',
DefaultWarnTime = 'ontime-default-warn-time',
DefaultDangerTime = 'ontime-default-danger-time',
DefaultPublic = 'ontime-default-public',
DefaultTimerType = 'ontime-default-timer-type',
DefaultEndAction = 'ontime-default-end-action',
}
@@ -55,7 +51,6 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => {
),
defaultWarnTime: localStorage.getItem(EditorSettingsKeys.DefaultWarnTime) ?? editorSettingsDefaults.warnTime,
defaultDangerTime: localStorage.getItem(EditorSettingsKeys.DefaultDangerTime) ?? editorSettingsDefaults.dangerTime,
defaultPublic: booleanFromLocalStorage(EditorSettingsKeys.DefaultPublic, editorSettingsDefaults.isPublic),
defaultTimerType: validateTimerType(
localStorage.getItem(EditorSettingsKeys.DefaultTimerType),
editorSettingsDefaults.timerType,
@@ -92,11 +87,6 @@ export const useEditorSettings = create<EditorSettingsStore>((set) => {
localStorage.setItem(EditorSettingsKeys.DefaultDangerTime, String(defaultDangerTime));
return { defaultDangerTime };
}),
setDefaultPublic: (defaultPublic) =>
set(() => {
localStorage.setItem(EditorSettingsKeys.DefaultPublic, String(defaultPublic));
return { defaultPublic };
}),
setDefaultTimerType: (defaultTimerType) =>
set(() => {
localStorage.setItem(EditorSettingsKeys.DefaultTimerType, String(defaultTimerType));
@@ -19,7 +19,6 @@ describe('cloneEvent()', () => {
linkStart: false,
countToEnd: false,
endAction: EndAction.None,
isPublic: false,
skip: false,
colour: 'F00',
revision: 10,
@@ -52,7 +51,6 @@ describe('cloneEvent()', () => {
countToEnd: original.countToEnd,
linkStart: original.linkStart,
endAction: original.endAction,
isPublic: original.isPublic,
skip: original.skip,
colour: original.colour,
revision: 0,
-1
View File
@@ -20,7 +20,6 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
countToEnd: event.countToEnd,
linkStart: event.linkStart,
endAction: event.endAction,
isPublic: event.isPublic,
skip: event.skip,
colour: event.colour,
parent: event.parent,
@@ -31,7 +31,6 @@ const staticSelectProperties = [
{ value: 'eventNow.title', label: 'Title' },
{ value: 'eventNow.cue', label: 'Cue' },
{ value: 'eventNow.countToEnd', label: 'Count to end' },
{ value: 'eventNow.isPublic', label: 'Is public' },
{ value: 'eventNow.note', label: 'Note' },
{ value: 'eventNow.colour', label: 'Colour' },
];
@@ -38,7 +38,6 @@ const eventStaticPropertiesNow = [
'{{eventNow.timeStart}}',
'{{eventNow.timeEnd}}',
'{{eventNow.duration}}',
'{{eventNow.isPublic}}',
'{{eventNow.colour}}',
'{{eventNow.delay}}',
];
@@ -51,7 +50,6 @@ const eventStaticPropertiesNext = [
'{{eventNext.timeStart}}',
'{{eventNext.timeEnd}}',
'{{eventNext.duration}}',
'{{eventNext.isPublic}}',
'{{eventNext.colour}}',
'{{eventNext.delay}}',
];
@@ -13,7 +13,6 @@ export default function EditorSettingsForm() {
defaultTimeStrategy,
defaultWarnTime,
defaultDangerTime,
defaultPublic,
defaultTimerType,
defaultEndAction,
setDefaultDuration,
@@ -21,7 +20,6 @@ export default function EditorSettingsForm() {
setTimeStrategy,
setWarnTime,
setDangerTime,
setDefaultPublic,
setDefaultTimerType,
setDefaultEndAction,
} = useEditorSettings((state) => state);
@@ -127,17 +125,6 @@ export default function EditorSettingsForm() {
/>
</Panel.ListItem>
</Panel.ListGroup>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='Default public' description='New events will be public' />
<Switch
variant='ontime'
size='lg'
defaultChecked={defaultPublic}
onChange={(event) => setDefaultPublic(event.target.checked)}
/>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
<Panel.Section>
<Panel.Title>Run mode</Panel.Title>
@@ -8,7 +8,7 @@ import { PROJECT_LIST } from '../../../../common/api/constants';
import { createProject } from '../../../../common/api/db';
import { maybeAxiosError } from '../../../../common/api/utils';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { documentationUrl, websiteUrl } from '../../../../externals';
import { documentationUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
@@ -20,8 +20,6 @@ interface ProjectCreateFromProps {
type ProjectCreateFormValues = {
title?: string;
description?: string;
publicInfo?: string;
publicUrl?: string;
backstageInfo?: string;
backstageUrl?: string;
custom?: { title: string; value: string }[];
@@ -120,28 +118,6 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
{...register('description')}
/>
</label>
<label>
Public info
<Textarea
variant='ontime-filled'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
autoComplete='off'
resize='none'
{...register('publicInfo')}
/>
</label>
<label>
Public QR code Url
<Input
variant='ontime-filled'
size='sm'
placeholder={websiteUrl}
autoComplete='off'
{...register('publicUrl')}
/>
</label>
<label>
Backstage info
<Textarea
@@ -10,7 +10,7 @@ import { maybeAxiosError } from '../../../../common/api/utils';
import useProjectData from '../../../../common/hooks-query/useProjectData';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { validateLogo } from '../../../../common/utils/uploadUtils';
import { documentationUrl, websiteUrl } from '../../../../externals';
import { documentationUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
@@ -198,28 +198,6 @@ export default function ProjectData() {
{...register('description')}
/>
</label>
<label>
Public info
<Textarea
variant='ontime-filled'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
autoComplete='off'
resize='none'
{...register('publicInfo')}
/>
</label>
<label>
Public QR code URL
<Input
variant='ontime-filled'
size='sm'
placeholder={websiteUrl}
autoComplete='off'
{...register('publicUrl')}
/>
</label>
<label>
Backstage info
<Textarea
@@ -12,7 +12,6 @@ describe('convertToImportMap', () => {
Duration: 'duration',
Cue: 'cue',
Title: 'title',
'Is Public': 'public',
Skip: 'skip',
Note: 'notes',
Colour: 'colour',
@@ -12,7 +12,6 @@ const namedImportMap = {
Cue: 'cue',
Title: 'title',
'Count to end': 'count to end',
'Is Public': 'public',
Skip: 'skip',
Note: 'notes',
Colour: 'colour',
@@ -50,7 +49,6 @@ export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
cue: namedImportMap.Cue,
title: namedImportMap.Title,
countToEnd: namedImportMap['Count to end'],
isPublic: namedImportMap['Is Public'],
skip: namedImportMap.Skip,
note: namedImportMap.Note,
colour: namedImportMap.Colour,
@@ -41,7 +41,6 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<th>Warning Time</th>
<th>Danger Time</th>
<th>Count to end</th>
<th>Is Public</th>
<th>Skip</th>
<th>Colour</th>
<th>Timer Type</th>
@@ -75,7 +74,6 @@ export default function PreviewRundown(props: PreviewRundownProps) {
eventIndex += 1;
const colour = entry.colour ? getAccessibleColour(entry.colour) : {};
const countToEnd = booleanToText(entry.countToEnd);
const isPublic = booleanToText(entry.isPublic);
const skip = booleanToText(entry.skip);
return (
@@ -98,7 +96,6 @@ export default function PreviewRundown(props: PreviewRundownProps) {
<td>{millisToString(entry.timeWarning)}</td>
<td>{millisToString(entry.timeDanger)}</td>
<td className={style.center}>{countToEnd && <Tag>{countToEnd}</Tag>}</td>
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
<td>{skip && <Tag>{skip}</Tag>}</td>
<td style={{ ...colour }}>{entry.colour}</td>
<td className={style.center}>
@@ -175,7 +175,6 @@ export default function RundownEntry(props: RundownEntryProps) {
timeStrategy={data.timeStrategy}
linkStart={data.linkStart}
countToEnd={data.countToEnd}
isPublic={data.isPublic}
endAction={data.endAction}
timerType={data.timerType}
title={data.title}
@@ -196,6 +195,7 @@ export default function RundownEntry(props: RundownEntryProps) {
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
actionHandler={actionHandler}
hasTriggers={data.triggers.length > 0}
/>
);
} else if (isOntimeDelay(data)) {
@@ -4,8 +4,6 @@ import {
IoDuplicateOutline,
IoFolder,
IoLink,
IoPeople,
IoPeopleOutline,
IoReorderTwo,
IoSwapVertical,
IoTrash,
@@ -36,7 +34,6 @@ interface EventBlockProps {
linkStart: boolean;
countToEnd: boolean;
eventIndex: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
title: string;
@@ -65,6 +62,7 @@ interface EventBlockProps {
value: unknown;
},
) => void;
hasTriggers: boolean;
}
export default function EventBlock(props: EventBlockProps) {
@@ -77,7 +75,6 @@ export default function EventBlock(props: EventBlockProps) {
timeStrategy,
linkStart,
countToEnd,
isPublic = true,
eventIndex,
endAction,
timerType,
@@ -99,6 +96,7 @@ export default function EventBlock(props: EventBlockProps) {
totalGap,
isLinkedToLoaded,
actionHandler,
hasTriggers,
} = props;
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
const { selectedEvents, setSelectedEvents } = useEventSelection();
@@ -126,25 +124,6 @@ export default function EventBlock(props: EventBlockProps) {
value: null,
}),
},
{
label: 'Make public',
icon: IoPeople,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: true,
}),
withDivider: true,
},
{
label: 'Make private',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: false,
}),
},
{ withDivider: true, label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
]
@@ -158,16 +137,6 @@ export default function EventBlock(props: EventBlockProps) {
value: linkStart,
}),
},
{
label: 'Toggle public',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: !isPublic,
}),
withDivider: true,
},
{
label: 'Add to swap',
icon: IoAdd,
@@ -308,7 +277,6 @@ export default function EventBlock(props: EventBlockProps) {
timeStrategy={timeStrategy}
eventId={eventId}
eventIndex={eventIndex}
isPublic={isPublic}
endAction={endAction}
timerType={timerType}
title={title}
@@ -323,6 +291,7 @@ export default function EventBlock(props: EventBlockProps) {
isPast={isPast}
totalGap={totalGap}
isLinkedToLoaded={isLinkedToLoaded}
hasTriggers={hasTriggers}
/>
)}
</div>
@@ -4,7 +4,7 @@ import {
IoArrowUp,
IoBan,
IoFlag,
IoPeople,
IoFlash,
IoPlay,
IoPlayForward,
IoPlaySkipForward,
@@ -33,7 +33,6 @@ interface EventBlockInnerProps {
linkStart: boolean;
countToEnd: boolean;
eventIndex: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
title: string;
@@ -48,6 +47,7 @@ interface EventBlockInnerProps {
isPast: boolean;
totalGap: number;
isLinkedToLoaded: boolean;
hasTriggers: boolean;
}
function EventBlockInner(props: EventBlockInnerProps) {
@@ -59,7 +59,6 @@ function EventBlockInner(props: EventBlockInnerProps) {
timeStrategy,
linkStart,
countToEnd,
isPublic = true,
endAction,
timerType,
title,
@@ -74,6 +73,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
isPast,
totalGap,
isLinkedToLoaded,
hasTriggers,
} = props;
const [renderInner, setRenderInner] = useState(false);
@@ -132,7 +132,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
duration={duration}
/>
)}
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
<div className={style.statusElements} id='block-status' data-timerType={timerType}>
<span className={style.eventNote}>{note}</span>
<div className={loaded ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
{loaded && <EventBlockProgressBar />}
@@ -153,9 +153,9 @@ function EventBlockInner(props: EventBlockInnerProps) {
<IoFlag className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
</span>
</Tooltip>
<Tooltip label={`${isPublic ? 'Event is public' : 'Event is private'}`} openDelay={tooltipDelayMid}>
<Tooltip label='Event has Triggers' openDelay={tooltipDelayMid}>
<span>
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : style.disabled}`} />
<IoFlash className={`${style.statusIcon} ${hasTriggers ? style.active : style.disabled}`} />
</span>
</Tooltip>
</div>
@@ -56,7 +56,6 @@ export default function EventEditor(props: EventEditorProps) {
linkStart={event.linkStart}
countToEnd={event.countToEnd}
delay={event.delay ?? 0}
isPublic={event.isPublic}
endAction={event.endAction}
timerType={event.timerType}
timeWarning={event.timeWarning}
@@ -21,14 +21,13 @@ interface EventEditorTimesProps {
linkStart: boolean;
countToEnd: boolean;
delay: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
timeWarning: number;
timeDanger: number;
}
type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger';
type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'timeWarning' | 'timeDanger';
function EventEditorTimes(props: EventEditorTimesProps) {
const {
@@ -40,7 +39,6 @@ function EventEditorTimes(props: EventEditorTimesProps) {
linkStart,
countToEnd,
delay,
isPublic,
endAction,
timerType,
timeWarning,
@@ -49,11 +47,6 @@ function EventEditorTimes(props: EventEditorTimesProps) {
const { updateEntry } = useEntryActions();
const handleSubmit = (field: HandledActions, value: string | boolean) => {
if (field === 'isPublic') {
updateEntry({ id: eventId, isPublic: !(value as boolean) });
return;
}
if (field === 'countToEnd') {
updateEntry({ id: eventId, countToEnd: !(value as boolean) });
return;
@@ -160,6 +153,9 @@ function EventEditorTimes(props: EventEditorTimesProps) {
<option value={TimerType.None}>None</option>
</Select>
</div>
<div>
{/* TODO: rearrange this grid */}
</div>
<div>
<Editor.Label htmlFor='timeWarning'>Warning Time</Editor.Label>
<TimeInput
@@ -171,19 +167,6 @@ function EventEditorTimes(props: EventEditorTimesProps) {
/>
</div>
<div>
<Editor.Label htmlFor='isPublic'>Event Visibility</Editor.Label>
<Editor.Label className={style.switchLabel}>
<Switch
id='isPublic'
size='md'
isChecked={isPublic}
onChange={() => handleSubmit('isPublic', isPublic)}
variant='ontime'
/>
{isPublic ? 'Public' : 'Private'}
</Editor.Label>
</div>
<div>
<Editor.Label htmlFor='timeDanger'>Danger Time</Editor.Label>
<TimeInput
@@ -20,7 +20,6 @@ function QuickAddBlock(props: QuickAddBlockProps) {
const { addEntry } = useEntryActions();
const doLinkPrevious = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null);
const addEvent = () => {
addEntry(
@@ -30,7 +29,6 @@ function QuickAddBlock(props: QuickAddBlockProps) {
},
{
after: previousEventId,
defaultPublic: doPublic?.current?.checked,
lastEventId: previousEventId,
linkPrevious: doLinkPrevious?.current?.checked,
},
@@ -1,4 +1,4 @@
import { ComponentType, useMemo } from 'react';
import { ComponentType } from 'react';
import { ViewExtendedTimer } from 'common/models/TimeManager.type';
import {
CustomFields,
@@ -8,7 +8,6 @@ import {
Runtime,
Settings,
SimpleTimerState,
SupportedEntry,
TimerType,
ViewSettings,
} from 'ontime-types';
@@ -25,19 +24,15 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
type WithDataProps = {
auxTimer: SimpleTimerState;
backstageEvents: OntimeEvent[];
events: OntimeEvent[];
customFields: CustomFields;
eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null;
events: OntimeEvent[];
general: ProjectData;
isMirrored: boolean;
message: MessageState;
nextId: string | null;
onAir: boolean;
publicEventNext: OntimeEvent | null;
publicEventNow: OntimeEvent | null;
publicSelectedId: string | null;
runtime: Runtime;
selectedId: string | null;
settings: Settings | undefined; // TODO: what is the case for this being undefined?
@@ -61,17 +56,9 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
const { data: settings } = useSettings();
const { data: customFields } = useCustomFields();
const publicEvents = useMemo(() => {
if (Array.isArray(rundownData)) {
return rundownData.filter((e) => e.type === SupportedEntry.Event && e.title && e.isPublic);
}
return [];
}, [rundownData]);
// websocket data
const { clock, timer, message, onAir, eventNext, publicEventNext, publicEventNow, eventNow, runtime, auxtimer1 } =
const { clock, timer, message, onAir, eventNext, eventNow, runtime, auxtimer1 } =
useStore(runtimeStore);
const publicSelectedId = publicEventNow?.id ?? null;
const selectedId = eventNow?.id ?? null;
const nextId = eventNext?.id ?? null;
@@ -91,19 +78,15 @@ const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
<Component
{...props}
auxTimer={auxtimer1}
backstageEvents={rundownData}
events={rundownData}
customFields={customFields}
eventNext={eventNext}
eventNow={eventNow}
events={publicEvents}
general={project}
isMirrored={isMirrored}
message={message}
nextId={nextId}
onAir={onAir}
publicEventNext={publicEventNext}
publicEventNow={publicEventNow}
publicSelectedId={publicSelectedId}
runtime={runtime}
selectedId={selectedId}
settings={settings}
@@ -7,7 +7,6 @@ import {
ProjectData,
Runtime,
Settings,
SupportedEntry,
TimerPhase,
} from 'ontime-types';
@@ -27,7 +26,7 @@ import CountdownSelect from './CountdownSelect';
import './Countdown.scss';
interface CountdownProps {
backstageEvents: OntimeEvent[];
events: OntimeEvent[];
general: ProjectData;
isMirrored: boolean;
runtime: Runtime;
@@ -37,7 +36,7 @@ interface CountdownProps {
}
export default function Countdown(props: CountdownProps) {
const { backstageEvents, general, isMirrored, runtime, selectedId, settings, time } = props;
const { events, general, isMirrored, runtime, selectedId, settings, time } = props;
const [searchParams] = useSearchParams();
const { getLocalizedString } = useTranslation();
@@ -49,7 +48,7 @@ export default function Countdown(props: CountdownProps) {
// eg. http://localhost:4001/countdown?eventId=ei0us
// update data to the event we are following
useEffect(() => {
if (!backstageEvents) {
if (!events) {
return;
}
@@ -63,7 +62,6 @@ export default function Countdown(props: CountdownProps) {
}
let followThis: OntimeEvent | null = null;
const events: OntimeEvent[] = [...backstageEvents].filter((event) => event.type === SupportedEntry.Event);
if (eventId !== null) {
followThis = events.find((event) => event.id === eventId) || null;
@@ -72,11 +70,11 @@ export default function Countdown(props: CountdownProps) {
}
if (followThis !== null) {
setFollow(followThis);
const idx: number = backstageEvents.findIndex((event: OntimeEntry) => event.id === followThis?.id);
const delayToEvent = backstageEvents[idx]?.delay ?? 0;
const idx: number = events.findIndex((event: OntimeEntry) => event.id === followThis?.id);
const delayToEvent = events[idx]?.delay ?? 0;
setDelay(delayToEvent);
}
}, [backstageEvents, searchParams]);
}, [events, searchParams]);
const { message: runningMessage, timer: runningTimer } = fetchTimerData(time, follow, selectedId, runtime.offset);
@@ -119,7 +117,7 @@ export default function Countdown(props: CountdownProps) {
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />}
<ViewParamsEditor viewOptions={viewOptions} />
{follow === null ? (
<CountdownSelect events={backstageEvents} />
<CountdownSelect events={events} />
) : (
<div className='countdown-container' data-testid='countdown-event'>
<div className='clock-container'>
@@ -1,3 +1,5 @@
import { ViewExtendedTimer } from 'common/models/TimeManager.type';
import { OntimeEvent } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { fetchTimerData, sanitiseTitle, TimerMessage } from '../countdown.helpers';
@@ -14,7 +16,7 @@ describe('sanitiseTitle() function', () => {
it('should return {no title} when invalid', () => {
const invalidTitles = ['', undefined, null];
for (const title of invalidTitles) {
expect(sanitiseTitle(title)).toBe('{no title}');
expect(sanitiseTitle(title as unknown as string)).toBe('{no title}');
}
});
});
@@ -23,10 +25,10 @@ describe('fetchTimerData() function', () => {
it('shows current timer if current is the one we follow', () => {
const followId = 'testId';
const currentMockValue = 13;
const follow = { id: followId };
const time = { current: currentMockValue };
const follow = { id: followId } as OntimeEvent;
const time = { current: currentMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, followId);
const { message, timer } = fetchTimerData(time, follow, followId, 0);
expect(message).toBe(TimerMessage.running);
expect(timer).toBe(currentMockValue);
});
@@ -34,10 +36,10 @@ describe('fetchTimerData() function', () => {
it('shows the countdown to an upcoming event', () => {
const startMockValue = 10000;
const timeNow = 1000;
const follow = { id: 'anotherevent', timeStart: startMockValue };
const time = { clock: timeNow };
const follow = { id: 'anotherevent', timeStart: startMockValue } as OntimeEvent;
const time = { clock: timeNow } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent', 0);
expect(message).toBe(TimerMessage.toStart);
expect(timer).toBe(startMockValue - timeNow);
});
@@ -47,10 +49,10 @@ describe('fetchTimerData() function', () => {
const endMockValue = 20000;
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: endMockValue - startMockValue };
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue } as OntimeEvent;
const time = { clock: timeNow, current: endMockValue - startMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent', 0);
expect(message).toBe(TimerMessage.waiting);
expect(timer).toBe(endMockValue - startMockValue);
});
@@ -60,10 +62,10 @@ describe('fetchTimerData() function', () => {
const endMockValue = 20000;
const timeNow = 30000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: endMockValue - startMockValue };
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue } as OntimeEvent;
const time = { clock: timeNow, current: endMockValue - startMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent', 0);
expect(message).toBe(TimerMessage.ended);
expect(timer).toBe(endMockValue);
});
@@ -73,10 +75,10 @@ describe('fetchTimerData() function', () => {
const endMockValue = 1000;
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue } as OntimeEvent;
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent', 0);
expect(message).toBe(TimerMessage.waiting);
expect(timer).toBe(dayInMs + endMockValue - startMockValue);
});
@@ -86,10 +88,10 @@ describe('fetchTimerData() function', () => {
const endMockValue = 1000;
const timeNow = 15000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue } as OntimeEvent;
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, followId);
const { message, timer } = fetchTimerData(time, follow, followId, 0);
expect(message).toBe(TimerMessage.running);
expect(timer).toBe(dayInMs + endMockValue - startMockValue);
});
@@ -99,10 +101,10 @@ describe('fetchTimerData() function', () => {
const endMockValue = 1000;
const timeNow = 2000;
const followId = 'testId';
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue };
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue };
const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue } as OntimeEvent;
const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue } as ViewExtendedTimer;
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent');
const { message, timer } = fetchTimerData(time, follow, 'notthesameevent', 0);
expect(message).toBe(TimerMessage.toStart);
expect(timer).toBe(startMockValue - timeNow);
});
@@ -26,7 +26,7 @@ export default function LowerThird(props: LowerProps) {
const animationTimeout = useRef<NodeJS.Timeout>();
const [playState, setPlayState] = useState<boolean>(false);
const [textValue, setTextValue] = useState<{ top: string; bottom: string }>({ top: '', bottom: '' });
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
useRuntimeStylesheet(viewSettings?.overrideStyles ? overrideStylesURL : undefined);
const options = useLowerOptions();
const { playback } = time;
@@ -17,7 +17,7 @@ import StudioClockSchedule from './StudioClockSchedule';
import './StudioClock.scss';
interface StudioClockProps {
backstageEvents: OntimeEntry[];
events: OntimeEntry[];
eventNext: OntimeEvent | null;
general: ProjectData;
isMirrored: boolean;
@@ -29,7 +29,7 @@ interface StudioClockProps {
}
export default function StudioClock(props: StudioClockProps) {
const { backstageEvents, eventNext, general, isMirrored, time, selectedId, nextId, onAir, settings } = props;
const { events, eventNext, general, isMirrored, time, selectedId, nextId, onAir, settings } = props;
const [searchParams] = useSearchParams();
@@ -102,7 +102,7 @@ export default function StudioClock(props: StudioClockProps) {
</div>
</div>
{!hideRight && (
<StudioClockSchedule rundown={backstageEvents} selectedId={selectedId} nextId={nextId} onAir={onAir} />
<StudioClockSchedule rundown={events} selectedId={selectedId} nextId={nextId} onAir={onAir} />
)}
</div>
);
@@ -5,7 +5,6 @@ export const langDe: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Jetzt',
'common.next': 'Nächste',
'common.public_message': 'Öffentliche Nachricht',
'common.scheduled_start': 'Geplanter beginn',
'common.scheduled_end': 'Geplantes ende',
'common.projected_start': 'Erwartetes beginn',
@@ -28,6 +27,4 @@ export const langDe: TranslationObject = {
'project.description': 'Beschreibung',
'project.backstage_info': 'Backstage-Informationen',
'project.backstage_url': 'Backstage-URL',
'project.public_info': 'Öffentliche Informationen',
'project.public_url': 'Öffentliche URL',
};
@@ -3,7 +3,6 @@ export const langEn = {
'common.minutes': 'min',
'common.now': 'Now',
'common.next': 'Next',
'common.public_message': 'Public message',
'common.scheduled_start': 'Scheduled start',
'common.scheduled_end': 'Scheduled end',
'common.projected_start': 'Projected start',
@@ -26,8 +25,6 @@ export const langEn = {
'project.description': 'Description',
'project.backstage_info': 'Backstage Info',
'project.backstage_url': 'Backstage URL',
'project.public_info': 'Public Info',
'project.public_url': 'Public URL',
};
export type TranslationObject = Record<keyof typeof langEn, string>;
@@ -5,7 +5,6 @@ export const langEs: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Ahora',
'common.next': 'Siguiente',
'common.public_message': 'Mensaje público',
'common.scheduled_start': 'Inicio programado',
'common.scheduled_end': 'Fin programado',
'common.projected_start': 'Inicio previsto',
@@ -28,6 +27,4 @@ export const langEs: TranslationObject = {
'project.description': 'Descripción',
'project.backstage_info': 'Información de backstage',
'project.backstage_url': 'URL de backstage',
'project.public_info': 'Información pública',
'project.public_url': 'URL pública',
};
+1 -3
View File
@@ -5,7 +5,6 @@ export const langFr: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Maintenant',
'common.next': 'A suivre',
'common.public_message': 'Message public',
'common.scheduled_start': 'Début prévu',
'common.scheduled_end': 'Fin prévue',
'common.projected_start': 'Début projeté',
@@ -28,6 +27,5 @@ export const langFr: TranslationObject = {
'project.description': 'Description',
'project.backstage_info': 'Informations des coulisses',
'project.backstage_url': 'URL des coulisses',
'project.public_info': 'Informations publiques',
'project.public_url': 'URL publique',
};
@@ -5,7 +5,6 @@ export const langHu: TranslationObject = {
'common.minutes': 'perc',
'common.now': 'Most',
'common.next': 'Következő',
'common.public_message': 'Nyilvános közlemény',
'common.scheduled_start': 'Ütemezett kezdés',
'common.scheduled_end': 'Ütemezett befejezés',
'common.projected_start': 'Várható kezdés',
@@ -28,6 +27,4 @@ export const langHu: TranslationObject = {
'project.description': 'Leírás',
'project.backstage_info': 'Kulisszák mögötti információ',
'project.backstage_url': 'Kulisszák mögötti URL',
'project.public_info': 'Nyilvános információ',
'project.public_url': 'Nyilvános URL',
};
@@ -5,7 +5,6 @@ export const langIt: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Adesso',
'common.next': 'Prossimo',
'common.public_message': 'Messaggio pubblico',
'common.scheduled_start': 'Inizio programmato',
'common.scheduled_end': 'Fine programmata',
'common.projected_start': 'Inizio previsto',
@@ -28,6 +27,4 @@ export const langIt: TranslationObject = {
'project.description': 'Descrizione',
'project.backstage_info': 'Informazioni di backstage',
'project.backstage_url': 'URL di backstage',
'project.public_info': 'Informazioni pubbliche',
'project.public_url': 'URL pubblico',
};
@@ -5,7 +5,6 @@ export const langNo: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Nå',
'common.next': 'Neste',
'common.public_message': 'Offentlig beskjed',
'common.scheduled_start': 'Planlagt start',
'common.scheduled_end': 'Planlagt slutt',
'common.projected_start': 'Forventet start',
@@ -28,6 +27,4 @@ export const langNo: TranslationObject = {
'project.description': 'Beskrivelse',
'project.backstage_info': 'Backstage-informasjon',
'project.backstage_url': 'Backstage-URL',
'project.public_info': 'Offentlig informasjon',
'project.public_url': 'Offentlig URL',
};
@@ -5,7 +5,6 @@ export const langPl: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Teraz',
'common.next': 'Następnie',
'common.public_message': 'Wiadomość publiczna',
'common.scheduled_start': 'Planowany początek',
'common.scheduled_end': 'Planowany koniec',
'common.projected_start': 'Przewidywany początek',
@@ -28,6 +27,4 @@ export const langPl: TranslationObject = {
'project.description': 'Opis',
'project.backstage_info': 'Informacje zaplecza',
'project.backstage_url': 'URL zaplecza',
'project.public_info': 'Informacje publiczne',
'project.public_url': 'URL publiczny',
};
@@ -5,7 +5,6 @@ export const langPt: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Agora',
'common.next': 'Próximo',
'common.public_message': 'Mensagem pública',
'common.scheduled_start': 'Início programado',
'common.scheduled_end': 'Fim programado',
'common.projected_start': 'Início previsto',
@@ -28,6 +27,4 @@ export const langPt: TranslationObject = {
'project.description': 'Descrição',
'project.backstage_info': 'Informações de bastidores',
'project.backstage_url': 'URL de bastidores',
'project.public_info': 'Informações públicas',
'project.public_url': 'URL pública',
};
@@ -5,7 +5,6 @@ export const langSv: TranslationObject = {
'common.minutes': 'min',
'common.now': 'Nu',
'common.next': 'Nästa',
'common.public_message': 'Offentligt meddelande',
'common.scheduled_start': 'Planerad start',
'common.scheduled_end': 'Planerad slut',
'common.projected_start': 'Beräknad start',
@@ -28,6 +27,4 @@ export const langSv: TranslationObject = {
'project.description': 'Beskrivning',
'project.backstage_info': 'Backstageinformation',
'project.backstage_url': 'Backstage-URL',
'project.public_info': 'Offentlig information',
'project.public_url': 'Offentlig URL',
};
@@ -5,7 +5,6 @@ export const langZhCn: TranslationObject = {
'common.minutes': '分钟',
'common.now': '现在',
'common.next': '接下来',
'common.public_message': '公众消息',
'common.scheduled_start': '计划开始',
'common.scheduled_end': '计划结束',
'common.projected_start': '预计开始',
@@ -28,6 +27,4 @@ export const langZhCn: TranslationObject = {
'project.description': '描述',
'project.backstage_info': '后台信息',
'project.backstage_url': '后台网址',
'project.public_info': '公开信息',
'project.public_url': '公开网址',
};
-1
View File
@@ -4,7 +4,6 @@ export const navigatorConstants = [
{ url: 'clock', label: 'Wall Clock' },
{ url: 'backstage', label: 'Backstage' },
{ url: 'timeline', label: 'Timeline (beta)' },
{ url: 'public', label: 'Public' },
{ url: 'lower', label: 'Lower Thirds' },
{ url: 'studio', label: 'Studio Clock' },
{ url: 'countdown', label: 'Countdown' },
+1 -1
View File
@@ -8,7 +8,7 @@ import style from './ViewLoader.module.scss';
export default function ViewLoader({ children }: PropsWithChildren) {
const { data } = useViewSettings();
const { shouldRender } = useRuntimeStylesheet(data.overrideStyles && overrideStylesURL);
const { shouldRender } = useRuntimeStylesheet(data.overrideStyles ? overrideStylesURL : undefined);
// eventually we would want to leverage suspense here
// while the feature is not ready, we simply trigger a loader
@@ -23,7 +23,7 @@ import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from '
import './Backstage.scss';
interface BackstageProps {
backstageEvents: OntimeEvent[];
events: OntimeEvent[];
customFields: CustomFields;
eventNext: OntimeEvent | null;
eventNow: OntimeEvent | null;
@@ -37,7 +37,7 @@ interface BackstageProps {
export default function Backstage(props: BackstageProps) {
const {
backstageEvents,
events,
customFields,
eventNext,
eventNow,
@@ -68,7 +68,7 @@ export default function Backstage(props: BackstageProps) {
}, [selectedId]);
// gather card data
const hasEvents = backstageEvents.length > 0;
const hasEvents = events.length > 0;
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
eventNow,
eventNext,
@@ -176,7 +176,7 @@ export default function Backstage(props: BackstageProps) {
)}
</div>
{showSchedule && <ScheduleExport selectedId={selectedId} isBackstage />}
{showSchedule && <ScheduleExport selectedId={selectedId} />}
<div className={cx(['info', !showSchedule && 'info--stretch'])}>
{general.backstageUrl && <QRCode value={general.backstageUrl} size={qrSize} level='L' className='qr' />}
@@ -7,12 +7,11 @@ import ScheduleItem from './ScheduleItem';
import './Schedule.scss';
interface ScheduleProps {
isProduction?: boolean;
className?: string;
}
export default function Schedule({ isProduction, className }: ScheduleProps) {
const { events, isBackstage, containerRef } = useSchedule();
export default function Schedule({ className }: ScheduleProps) {
const { events, containerRef } = useSchedule();
if (events?.length < 1) {
return null;
@@ -21,7 +20,7 @@ export default function Schedule({ isProduction, className }: ScheduleProps) {
return (
<ul className={cx(['schedule', className])} ref={containerRef}>
{events.map((event) => {
const { timeStart, timeEnd, delay } = getScheduledTimes(event, isProduction);
const { timeStart, timeEnd, delay } = getScheduledTimes(event);
return (
<ScheduleItem
@@ -29,8 +28,7 @@ export default function Schedule({ isProduction, className }: ScheduleProps) {
timeStart={timeStart}
timeEnd={timeEnd}
title={event.title}
colour={isBackstage ? event.colour : undefined}
backstageEvent={!event.isPublic}
colour={event.colour}
skip={event.skip}
delay={delay}
/>
@@ -19,7 +19,6 @@ interface ScheduleContextState {
selectedEventId: string | null;
numPages: number;
visiblePage: number;
isBackstage: boolean;
containerRef: RefObject<HTMLUListElement>;
}
@@ -27,20 +26,12 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps {
selectedEventId: string | null;
isBackstage?: boolean;
}
export const ScheduleProvider = ({
children,
selectedEventId,
isBackstage = false,
}: PropsWithChildren<ScheduleProviderProps>) => {
export const ScheduleProvider = ({ children, selectedEventId }: PropsWithChildren<ScheduleProviderProps>) => {
const { cycleInterval, stopCycle } = useScheduleOptions();
const { data: events } = usePartialRundown((event: OntimeEntry) => {
if (isBackstage) {
return isOntimeEvent(event);
}
return isOntimeEvent(event) && event.isPublic && !event.skip;
return isOntimeEvent(event);
});
const [firstIndex, setFirstIndex] = useState(-1);
@@ -150,7 +141,6 @@ export const ScheduleProvider = ({
selectedEventId,
numPages,
visiblePage,
isBackstage,
containerRef,
}}
>
@@ -7,16 +7,15 @@ import ScheduleNav from './ScheduleNav';
interface ScheduleExportProps {
selectedId: MaybeString;
isBackstage?: boolean;
}
export default memo(ScheduleExport);
function ScheduleExport(props: ScheduleExportProps) {
const { selectedId, isBackstage } = props;
const { selectedId } = props;
return (
<ScheduleProvider selectedEventId={selectedId} isBackstage={isBackstage}>
<ScheduleProvider selectedEventId={selectedId}>
<ScheduleNav className='schedule-nav-container' />
<Schedule isProduction={isBackstage} className='schedule-container' />
<Schedule className='schedule-container' />
</ScheduleProvider>
);
}
@@ -16,14 +16,13 @@ interface ScheduleItemProps {
timeStart: number;
timeEnd: number;
title: string;
backstageEvent: boolean;
colour?: string;
skip?: boolean;
delay: number;
}
export default function ScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, backstageEvent, colour, skip, delay } = props;
const { timeStart, timeEnd, title, colour, skip, delay } = props;
const { showProjected } = useScheduleOptions();
if (showProjected) {
@@ -33,7 +32,6 @@ export default function ScheduleItem(props: ScheduleItemProps) {
timeEnd={timeEnd}
title={title}
colour={colour}
backstageEvent={backstageEvent}
skip={skip}
delay={delay}
/>
@@ -47,7 +45,6 @@ export default function ScheduleItem(props: ScheduleItemProps) {
timeEnd={timeEnd}
title={title}
colour={colour}
backstageEvent={backstageEvent}
skip={skip}
delay={delay}
/>
@@ -63,7 +60,6 @@ export default function ScheduleItem(props: ScheduleItemProps) {
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
{backstageEvent && '*'}
</div>
<div className='entry-title'>{title}</div>
</li>
@@ -71,7 +67,7 @@ export default function ScheduleItem(props: ScheduleItemProps) {
}
function DelayedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, backstageEvent, colour, skip, delay } = props;
const { timeStart, timeEnd, title, colour, skip, delay } = props;
const start = formatTime(timeStart, formatOptions);
const end = formatTime(timeEnd, formatOptions);
@@ -86,13 +82,11 @@ function DelayedScheduleItem(props: ScheduleItemProps) {
<SuperscriptTime time={start} />
<SuperscriptTime time={end} />
{backstageEvent && '*'}
</span>
<span className='entry-times--delay'>
<SuperscriptTime time={delayedStart} />
<SuperscriptTime time={delayedEnd} />
{backstageEvent && '*'}
</span>
</div>
<div className='entry-title'>{title}</div>
@@ -101,7 +95,7 @@ function DelayedScheduleItem(props: ScheduleItemProps) {
}
function ProjectedScheduleItem(props: ScheduleItemProps) {
const { timeStart, timeEnd, title, backstageEvent, colour, skip, delay } = props;
const { timeStart, timeEnd, title, colour, skip, delay } = props;
return (
<li className={cx(['entry', skip && 'entry--skip'])}>
@@ -110,7 +104,6 @@ function ProjectedScheduleItem(props: ScheduleItemProps) {
<ProjectedTime time={timeStart} delay={delay} />
<ProjectedTime time={timeEnd} delay={delay} />
{backstageEvent && '*'}
</div>
<div className='entry-title'>{title}</div>
</li>
@@ -3,17 +3,10 @@ import { OntimeEvent } from 'ontime-types';
/**
* Gather rules for how to present scheduled times
*/
export function getScheduledTimes(event: OntimeEvent, isProduction?: boolean) {
if (isProduction) {
return {
timeStart: event.timeStart,
timeEnd: event.timeEnd,
delay: event.skip ? 0 : event.delay,
};
}
export function getScheduledTimes(event: OntimeEvent) {
return {
timeStart: event.timeStart,
timeEnd: event.timeEnd,
delay: 0,
delay: event.skip ? 0 : event.delay,
};
}
@@ -12,22 +12,6 @@ describe('parseField()', () => {
expect(parseField('duration', testData3)).toBe('00:10:00');
});
describe('returns an x when isPublic is truthy, empty string otherwise', () => {
const testTruthy = [1, true, 'x', 'test'];
const testFalsy = ['', null, undefined, false, 0];
testTruthy.forEach((value) => {
test(`${value}`, () => {
expect(parseField('isPublic', value)).toBe('x');
});
});
testFalsy.forEach((value) => {
test(`${value}`, () => {
expect(parseField('isPublic', value)).toBe('');
});
});
});
it('returns an empty string on undefined fields', () => {
// @ts-expect-error -- testing user data with missing fields
expect(parseField('title')).toBe('');
@@ -61,7 +45,7 @@ describe('makeTable()', () => {
title: 'test title 1',
timeStart: 0,
timeEnd: 0,
isPublic: 'x',
skip: true,
lighting: { value: 'test lighting' },
sound: { value: 'test sound' },
},
@@ -93,7 +77,6 @@ describe('makeTable()', () => {
"Cue",
"Title",
"Note",
"Is Public? (x)",
"Skip?",
"lighting",
"Type",
@@ -110,7 +93,6 @@ describe('makeTable()', () => {
"x",
"",
"",
"",
],
]
`);
@@ -23,7 +23,7 @@ export const parseField = (field: CsvHeaderKey, data: unknown): string => {
return millisToString(data as MaybeNumber, { fallback: '' });
}
if (field === 'isPublic' || field === 'skip') {
if (field === 'skip') {
return data ? 'x' : '';
}
@@ -52,7 +52,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeEntry[], custo
'cue',
'title',
'note',
'isPublic',
'skip',
...customFieldKeys,
'type',
@@ -67,7 +66,6 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeEntry[], custo
'Cue',
'Title',
'Note',
'Is Public? (x)',
'Skip?',
...customFieldLabels,
'Type',
@@ -9,7 +9,6 @@ import { useTranslation } from '../../translation/TranslationProvider';
import BackstageInfo from './backstage-info/BackstageInfo';
import CustomInfo from './custom-info/CustomInfo';
import PublicInfo from './public-info/PublicInfo';
import { projectInfoOptions } from './projectInfo.options';
import './ProjectInfo.scss';
@@ -66,7 +65,6 @@ export default function ProjectInfo(props: ProjectInfoProps) {
</>
)}
<BackstageInfo general={general} />
<PublicInfo general={general} />
<CustomInfo general={general} />
</div>
</div>
@@ -13,13 +13,6 @@ export const projectInfoOptions: ViewOption[] = [
type: 'boolean',
defaultValue: false,
},
{
id: 'showPublic',
title: 'Show Public Data',
description: 'Whether to show fields related to the public views',
type: 'boolean',
defaultValue: false,
},
{
id: 'showCustom',
title: 'Show Custom Data',
@@ -1,40 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { ProjectData } from 'ontime-types';
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
import { useTranslation } from '../../../translation/TranslationProvider';
interface PublicInfoProps {
general: ProjectData;
}
export default function PublicInfo(props: PublicInfoProps) {
const { general } = props;
const [searchParams] = useSearchParams();
const { getLocalizedString } = useTranslation();
const showPublic = isStringBoolean(searchParams.get('showPublic'));
if (!showPublic) {
return null;
}
return (
<>
{general.publicInfo && (
<>
<div className='info__label'>{getLocalizedString('project.public_info')}</div>
<div className='info__value'>{general.publicInfo}</div>
</>
)}
{general.publicUrl && (
<>
<div className='info__label'>{getLocalizedString('project.public_url')}</div>
<a href={general.publicUrl} target='_blank' rel='noreferrer' className='info__value'>
{general.publicUrl}
</a>
</>
)}
</>
);
}
-204
View File
@@ -1,204 +0,0 @@
@use '../../theme/viewerDefs' as *;
.public-screen {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
font-family: var(--font-family-override, $viewer-font-family);
background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
gap: $view-element-gap;
padding: $view-outer-padding;
font-size: $base-font-size;
display: grid;
grid-template-columns: 1fr 40vw;
grid-template-rows: auto 12px 1fr auto;
grid-template-areas:
'header header'
'now schedule-nav'
'info schedule';
.empty-container {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-top: 25vh;
color: var(--label-color-override, $viewer-label-color);
}
/* =================== HEADER + EXTRAS ===================*/
.project-header {
grid-area: header;
font-size: $header-font-size;
font-weight: 600;
display: flex;
gap: 1rem;
}
.logo {
max-width: min(200px, 20vw);
}
.title {
line-height: 1.1em;
}
.clock-container {
margin-left: auto;
font-weight: 600;
.label {
font-size: $timer-label-size;
color: var(--label-color-override, $viewer-label-color);
text-transform: uppercase;
}
.time {
font-size: $timer-value-size;
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
line-height: 0.95em;
}
}
/* =================== MAIN - NOW ===================*/
.card-container {
grid-area: now;
display: flex;
flex-direction: column;
gap: $view-element-gap;
}
.event {
background-color: var(--card-background-color-override, $viewer-card-bg-color);
padding: $view-card-padding;
border-radius: $element-border-radius;
}
.timer-group {
border-top: 2px solid var(--background-color-override, $viewer-background-color);
margin-top: max(1vh, 16px);
padding-top: max(1vh, 16px);
display: flex;
row-gap: 0.5em;
}
.time-entry {
&__label {
font-size: $timer-label-size;
color: var(--label-color-override, $viewer-label-color);
font-weight: 600;
text-transform: uppercase;
}
&__value {
font-size: $base-font-size;
color: var(--secondary-color-override, $viewer-secondary-color);
letter-spacing: 0.05em;
line-height: 0.95em;
}
&--pending {
color: var(--timer-pending-color-override, $ontime-roll);
}
}
/* =================== MAIN - SCHEDULE ===================*/
$schedule-left-spacing: clamp(16px, 4vw, 64px);
.schedule-container {
grid-area: schedule;
overflow: hidden;
height: 100%;
padding-left: $schedule-left-spacing;
}
.schedule-nav-container {
grid-area: schedule-nav;
padding-left: $schedule-left-spacing;
}
/* =================== MAIN - INFO ===================*/
.info {
grid-area: info;
display: flex;
gap: max(1vw, 16px);
align-self: flex-end;
overflow: hidden;
align-items: end;
&__message {
font-size: $base-font-size;
line-height: 1.2em;
white-space: pre-line;
overflow: hidden;
flex: 1;
}
.qr {
padding: 0.5rem;
background-color: $ui-white;
border-radius: 2px;
}
}
}
/* =================== MOBILE ===================*/
@media screen and (max-width: 768px) {
.public-screen {
display: flex;
flex-direction: column;
overflow-y: auto;
.project-header {
flex-direction: column;
position: relative;
gap: 0.5rem;
}
.logo img{
height: min(50px, 10vh);
}
.timer-group {
flex-wrap: wrap;
}
.clock-container {
position: absolute;
top: 0;
right: 0;
}
.schedule-nav-container {
padding-left: 0;
justify-content: center;
}
.schedule-container {
padding-left: 0;
flex: 1;
}
.info {
width: 100%;
text-align: right;
}
.qr {
display: none;
}
.info--stretch {
flex: 1;
}
}
}
-126
View File
@@ -1,126 +0,0 @@
import QRCode from 'react-qr-code';
import { useViewportSize } from '@mantine/hooks';
import { CustomFields, OntimeEvent, ProjectData, Settings } from 'ontime-types';
import Empty from '../../common/components/state/Empty';
import TitleCard from '../../common/components/title-card/TitleCard';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import { ViewExtendedTimer } from '../../common/models/TimeManager.type';
import { cx } from '../../common/utils/styleUtils';
import { formatTime, getDefaultFormat } from '../../common/utils/time';
import SuperscriptTime from '../../features/viewers/common/superscript-time/SuperscriptTime';
import { useTranslation } from '../../translation/TranslationProvider';
import { getIsPendingStart } from '../backstage/backstage.utils';
import ScheduleExport from '../common/schedule/ScheduleExport';
import { getPublicOptions, usePublicOptions } from './public.options';
import { getCardData, getFirstStartTime } from './public.utils';
import './Public.scss';
interface BackstageProps {
customFields: CustomFields;
events: OntimeEvent[];
general: ProjectData;
isMirrored: boolean;
publicEventNow: OntimeEvent | null;
publicEventNext: OntimeEvent | null;
time: ViewExtendedTimer;
publicSelectedId: string | null;
settings: Settings | undefined;
}
export default function Public(props: BackstageProps) {
const {
customFields,
events,
general,
isMirrored,
publicEventNow,
publicEventNext,
time,
publicSelectedId,
settings,
} = props;
const { getLocalizedString } = useTranslation();
const { secondarySource } = usePublicOptions();
const { height: screenHeight } = useViewportSize();
useWindowTitle('Public Schedule');
// gather card data
const hasEvents = events.length > 0;
const { showNow, nowMain, nowSecondary, showNext, nextMain, nextSecondary } = getCardData(
publicEventNow,
publicEventNext,
'title',
secondarySource,
time.playback,
);
// gather timer data
const clock = formatTime(time.clock);
const isPendingStart = getIsPendingStart(time.playback, time.phase);
const scheduledStart = (() => {
if (showNow) return undefined;
if (!hasEvents) return undefined;
return getFirstStartTime(events[0]);
})();
// gather presentation styles
const qrSize = Math.max(window.innerWidth / 15, 72);
const showSchedule = hasEvents && screenHeight > 700; // in vertical screens we may not have space
const showPending = scheduledStart !== undefined;
// gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const publicOptions = getPublicOptions(defaultFormat, customFields);
return (
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
<ViewParamsEditor viewOptions={publicOptions} />
<div className='project-header'>
{general?.projectLogo ? <ViewLogo name={general.projectLogo} className='logo' /> : <div className='logo' />}
<div className='title'>{general.title}</div>
<div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div>
<SuperscriptTime time={clock} className='time' />
</div>
</div>
{!hasEvents && <Empty text={getLocalizedString('countdown.waiting')} className='empty-container' />}
<div className='card-container'>
{showNow && hasEvents && (
<TitleCard className='event now' label='now' title={nowMain} secondary={nowSecondary} />
)}
{showPending && (
<div className='event'>
<div className='title-card__placeholder'>{getLocalizedString('countdown.waiting')}</div>
<div className='timer-group'>
<div className='time-entry'>
<div className={cx(['time-entry__label', isPendingStart && 'time-entry--pending'])}>
{getLocalizedString('common.scheduled_start')}
</div>
<SuperscriptTime time={formatTime(scheduledStart)} className='time-entry__value' />
</div>
</div>
</div>
)}
{showNext && hasEvents && (
<TitleCard className='event next' label='next' title={nextMain} secondary={nextSecondary} />
)}
</div>
{showSchedule && <ScheduleExport selectedId={publicSelectedId} />}
<div className={cx(['info', !showSchedule && 'info--stretch'])}>
{general.publicUrl && <QRCode value={general.publicUrl} size={qrSize} level='L' className='qr' />}
{general.publicInfo && <div className='info__message'>{general.publicInfo}</div>}
</div>
</div>
);
}
@@ -1,58 +0,0 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { CustomFields, OntimeEvent } from 'ontime-types';
import {
getTimeOption,
makeOptionsFromCustomFields,
OptionTitle,
} from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/types';
import { scheduleOptions } from '../common/schedule/schedule.options';
export const getPublicOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
return [
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
{
title: OptionTitle.DataSources,
collapsible: true,
options: [
{
id: 'secondary-src',
title: 'Event secondary text',
description: 'Select the data source for auxiliary text shown in now and next cards',
type: 'option',
values: secondaryOptions,
defaultValue: '',
},
],
},
scheduleOptions,
];
};
type PublicOptions = {
secondarySource: keyof OntimeEvent | null;
};
/**
* Utility extract the view options from URL Params
* the names and fallback are manually matched with timerOptions
*/
function getOptionsFromParams(searchParams: URLSearchParams): PublicOptions {
// we manually make an object that matches the key above
return {
secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null,
};
}
/**
* Hook exposes the backstage view options
*/
export function usePublicOptions(): PublicOptions {
const [searchParams] = useSearchParams();
const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
return options;
}
@@ -1,45 +0,0 @@
import { OntimeEvent, Playback } from 'ontime-types';
import { enDash } from '../../common/utils/styleUtils';
import { getPropertyValue } from '../../features/viewers/common/viewUtils';
/**
* What should we be showing in the cards?
*/
export function getCardData(
eventNow: OntimeEvent | null,
eventNext: OntimeEvent | null,
mainSource: keyof OntimeEvent | null,
secondarySource: keyof OntimeEvent | null,
playback: Playback,
) {
if (playback === Playback.Stop) {
return {
showNow: false,
nowMain: undefined,
nowSecondary: undefined,
showNext: false,
nextMain: undefined,
nextSecondary: undefined,
};
}
// if we are loaded, we show the upcoming event as next
const nowMain = getPropertyValue(eventNow, mainSource ?? 'title') || enDash;
const nowSecondary = getPropertyValue(eventNow, secondarySource);
const nextMain = getPropertyValue(eventNext, mainSource ?? 'title') || enDash;
const nextSecondary = getPropertyValue(eventNext, secondarySource);
return {
showNow: eventNow !== null,
nowMain,
nowSecondary,
showNext: eventNext !== null,
nextMain,
nextSecondary,
};
}
export function getFirstStartTime(firstPublicEvent: OntimeEvent | null): number | undefined {
return firstPublicEvent?.timeStart;
}
@@ -17,7 +17,7 @@ import { getTimeToStart, getUpcomingEvents, useScopedRundown } from './timeline.
import './TimelinePage.scss';
interface TimelinePageProps {
backstageEvents: OntimeEvent[];
events: OntimeEvent[];
general: ProjectData;
runtime: Runtime;
selectedId: MaybeString;
@@ -31,9 +31,9 @@ interface TimelinePageProps {
* There is little point splitting or memoising top level elements
*/
export default function TimelinePage(props: TimelinePageProps) {
const { backstageEvents, general, runtime, selectedId, settings, time } = props;
const { events, general, runtime, selectedId, settings, time } = props;
// holds copy of the rundown with only relevant events
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(backstageEvents, selectedId);
const { scopedRundown, firstStart, totalDuration } = useScopedRundown(events, selectedId);
const { getLocalizedString } = useTranslation();
const clock = formatTime(time.clock);
@@ -15,13 +15,6 @@ export const getTimelineOptions = (timeFormat: string): ViewOption[] => {
type: 'boolean',
defaultValue: false,
},
{
id: 'hideBackstage',
title: 'Hide Private Events',
description: 'Whether to hide non-public events',
type: 'boolean',
defaultValue: false,
},
],
},
];
@@ -95,7 +95,6 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
return { scopedRundown: [], firstStart: 0, totalDuration: 0 };
}
const hideBackstage = isStringBoolean(searchParams.get('hideBackstage'));
const hidePast = isStringBoolean(searchParams.get('hidePast'));
const scopedRundown: PlayableEvent[] = [];
@@ -117,11 +116,6 @@ export function useScopedRundown(rundown: OntimeEntry[], selectedEventId: MaybeS
continue;
}
// maybe filter backstage
if (!currentEntry.isPublic && hideBackstage) {
continue;
}
// add to scopedRundown
scopedRundown.push(currentEntry);
+1 -1
View File
@@ -42,6 +42,6 @@
],
"exclude": [
"node_modules",
"build"
"build",
]
}
+6
View File
@@ -61,6 +61,12 @@ export default defineConfig({
configure: logProxyRequests,
ws: true,
},
'^/user*': {
target: 'http://localhost:4001/',
changeOrigin: true,
configure: logProxyRequests,
ws: true,
},
},
},
test: {
@@ -148,7 +148,6 @@ function makeViewMenu(clientUrl) {
return {
label: 'Views',
submenu: [
makeItemOpenInBrowser('Public', `${clientUrl}/public`),
makeItemOpenInBrowser('Lower Thirds', `${clientUrl}/lower`),
{ type: 'separator' },
makeItemOpenInBrowser('Timer', `${clientUrl}/timer`),
@@ -53,8 +53,6 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
project: {
title: req.body?.title ?? '',
description: req.body?.description ?? '',
publicUrl: req.body?.publicUrl ?? '',
publicInfo: req.body?.publicInfo ?? '',
backstageUrl: req.body?.backstageUrl ?? '',
backstageInfo: req.body?.backstageInfo ?? '',
projectLogo: req.body?.projectLogo ?? null,
@@ -11,8 +11,6 @@ export const validateNewProject = [
body('filename').optional().isString().trim(),
body('title').optional().isString().trim(),
body('description').optional().isString().trim(),
body('publicUrl').optional().isString().trim(),
body('publicInfo').optional().isString().trim(),
body('backstageUrl').optional().isString().trim(),
body('backstageInfo').optional().isString().trim(),
body('projectLogo').optional().isString().trim(),
@@ -56,7 +56,6 @@ describe('parseExcel()', () => {
title: 'Guest Welcome',
timerType: 'count-down',
endAction: 'none',
isPublic: true,
skip: false,
note: 'Ballyhoo',
custom: {
@@ -76,7 +75,6 @@ describe('parseExcel()', () => {
title: 'A song from the hearth',
timerType: 'clock',
endAction: 'load-next',
isPublic: false,
skip: true,
note: 'Rainbow chase',
custom: {},
@@ -117,7 +115,6 @@ describe('parseExcel()', () => {
title: 'Guest Welcome',
timerType: 'count-down',
endAction: 'none',
isPublic: true,
skip: false,
note: 'Ballyhoo',
custom: {},
@@ -132,7 +129,6 @@ describe('parseExcel()', () => {
title: 'A song from the hearth',
timerType: 'clock',
endAction: 'load-next',
isPublic: false,
skip: true,
note: 'Rainbow chase',
custom: {},
@@ -434,7 +430,6 @@ describe('getCustomFieldData()', () => {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
@@ -487,7 +482,6 @@ describe('getCustomFieldData()', () => {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
@@ -9,7 +9,6 @@ export const dataFromExcelTemplate = [
'End Action',
'Timer type',
'Count to end',
'Public',
'Skip',
'Notes',
't0',
@@ -27,7 +26,6 @@ export const dataFromExcelTemplate = [
'', // <-- endAction
'', // <-- timerType
'x', // <-- count to end
'x', // <-- public
'', // <-- skip
'Ballyhoo', // <-- notes
'a0', // <-- t0
@@ -45,7 +43,6 @@ export const dataFromExcelTemplate = [
'load-next', // <-- endAction
'clock', // timerType
'x', // <-- count to end
'', // <-- public
'x', // <-- skip
'Rainbow chase', // <-- notes
'b0', // <-- t0
@@ -69,7 +69,6 @@ export const parseExcel = (
let colourIndex: number | null = null;
// options: booleans
let isPublicIndex: number | null = null;
let skipIndex: number | null = null;
let countToEndIndex: number | null = null;
@@ -128,10 +127,6 @@ export const parseExcel = (
countToEndIndex = col;
rundownMetadata['countToEnd'] = { row, col };
},
[importMap.isPublic]: (row: number, col: number) => {
isPublicIndex = col;
rundownMetadata['isPublic'] = { row, col };
},
[importMap.skip]: (row: number, col: number) => {
skipIndex = col;
rundownMetadata['skip'] = { row, col };
@@ -203,8 +198,6 @@ export const parseExcel = (
entry.cue = makeString(column, '');
} else if (j === countToEndIndex) {
entry.countToEnd = parseBooleanString(column);
} else if (j === isPublicIndex) {
entry.isPublic = parseBooleanString(column);
} else if (j === skipIndex) {
entry.skip = parseBooleanString(column);
} else if (j === notesIndex) {
@@ -17,8 +17,6 @@ export function parseProjectData(data: Partial<DatabaseModel>, emitError?: Error
return {
title: data.project.title ?? dbModel.project.title,
description: data.project.description ?? dbModel.project.description,
publicUrl: data.project.publicUrl ?? dbModel.project.publicUrl,
publicInfo: data.project.publicInfo ?? dbModel.project.publicInfo,
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo,
@@ -21,8 +21,6 @@ router.post('/', projectSanitiser, async (req: Request, res: Response<ProjectDat
const newData: Partial<ProjectData> = removeUndefined({
title: req.body?.title,
description: req.body?.description,
publicUrl: req.body?.publicUrl,
publicInfo: req.body?.publicInfo,
backstageUrl: req.body?.backstageUrl,
backstageInfo: req.body?.backstageInfo,
endMessage: req.body?.endMessage,
@@ -5,8 +5,6 @@ export const projectSanitiser = [
body().notEmpty().withMessage('No object found in request'),
body('title').optional().isString().trim(),
body('description').optional().isString().trim(),
body('publicUrl').optional().isString().trim(),
body('publicInfo').optional().isString().trim(),
body('backstageUrl').optional().isString().trim(),
body('backstageInfo').optional().isString().trim(),
body('endMessage').optional().isString().trim(),
@@ -20,7 +20,6 @@ describe('test event validator', () => {
timeStart: expect.any(Number),
timeEnd: expect.any(Number),
countToEnd: expect.any(Boolean),
isPublic: expect.any(Boolean),
skip: expect.any(Boolean),
revision: expect.any(Number),
type: expect.any(String),
@@ -107,7 +106,6 @@ describe('doesInvalidateMetadata()', () => {
note: 'note',
endAction: EndAction.LoadNext,
timerType: TimerType.Clock,
isPublic: false,
colour: 'colour',
timeWarning: 1,
timeDanger: 2,
@@ -83,7 +83,6 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
endAction: validateEndAction(patchEvent.endAction, originalEvent.endAction),
timerType: validateTimerType(patchEvent.timerType, originalEvent.timerType),
countToEnd: typeof patchEvent.countToEnd === 'boolean' ? patchEvent.countToEnd : originalEvent.countToEnd,
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
note: makeString(patchEvent.note, originalEvent.note),
colour: makeString(patchEvent.colour, originalEvent.colour),
@@ -214,7 +213,6 @@ enum RegenerateWhitelist {
'endAction',
'timerType',
'countToEnd',
'isPublic',
'colour',
'timeWarning',
'timeDanger',
@@ -22,7 +22,6 @@ const propertyConversion = {
note: coerceString,
cue: coerceString,
isPublic: coerceBoolean,
skip: coerceBoolean,
colour: coerceColour,
-2
View File
@@ -185,9 +185,7 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb
block: null,
startedAt: null,
},
publicEventNow: state.publicEventNow,
eventNext: state.eventNext,
publicEventNext: state.publicEventNext,
auxtimer1: {
duration: timerConfig.auxTimerDefault,
current: timerConfig.auxTimerDefault,
@@ -42,7 +42,6 @@ describe('safeMerge', () => {
const mergedData = safeMerge(demoDb, {
project: {
title: 'new title',
publicInfo: 'new public info',
backstageInfo: 'new backstage info',
custom: [
{
@@ -56,8 +55,6 @@ describe('safeMerge', () => {
expect(mergedData.project).toStrictEqual({
title: 'new title',
description: 'Turin 2022',
publicUrl: 'www.getontime.no',
publicInfo: 'new public info',
backstageUrl: 'www.github.com/cpvalente/ontime',
backstageInfo: 'new backstage info',
projectLogo: null,
-2
View File
@@ -17,8 +17,6 @@ export const dbModel: DatabaseModel = {
project: {
title: '',
description: '',
publicUrl: '',
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
projectLogo: null,
-16
View File
@@ -71,7 +71,6 @@ export const demoDb: DatabaseModel = {
timeStart: 36000000,
timeEnd: 37200000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -101,7 +100,6 @@ export const demoDb: DatabaseModel = {
timeStart: 37500000,
timeEnd: 38700000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -131,7 +129,6 @@ export const demoDb: DatabaseModel = {
timeStart: 39000000,
timeEnd: 40200000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -161,7 +158,6 @@ export const demoDb: DatabaseModel = {
timeStart: 40500000,
timeEnd: 41700000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -191,7 +187,6 @@ export const demoDb: DatabaseModel = {
timeStart: 42000000,
timeEnd: 43200000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -238,7 +233,6 @@ export const demoDb: DatabaseModel = {
timeStart: 47100000,
timeEnd: 48300000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -268,7 +262,6 @@ export const demoDb: DatabaseModel = {
timeStart: 48600000,
timeEnd: 49800000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -298,7 +291,6 @@ export const demoDb: DatabaseModel = {
timeStart: 50100000,
timeEnd: 51300000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -328,7 +320,6 @@ export const demoDb: DatabaseModel = {
timeStart: 51600000,
timeEnd: 52800000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -358,7 +349,6 @@ export const demoDb: DatabaseModel = {
timeStart: 53100000,
timeEnd: 54300000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -405,7 +395,6 @@ export const demoDb: DatabaseModel = {
timeStart: 56100000,
timeEnd: 57300000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -435,7 +424,6 @@ export const demoDb: DatabaseModel = {
timeStart: 57600000,
timeEnd: 58800000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -465,7 +453,6 @@ export const demoDb: DatabaseModel = {
timeStart: 59100000,
timeEnd: 60300000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -495,7 +482,6 @@ export const demoDb: DatabaseModel = {
timeStart: 60600000,
timeEnd: 61800000,
duration: 1200000,
isPublic: true,
skip: false,
colour: '',
parent: null,
@@ -518,8 +504,6 @@ export const demoDb: DatabaseModel = {
project: {
title: 'Eurovision Song Contest',
description: 'Turin 2022',
publicUrl: 'www.getontime.no',
publicInfo: 'Rehearsal Schedule - Turin 2022',
backstageUrl: 'www.github.com/cpvalente/ontime',
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
projectLogo: null,
@@ -20,7 +20,6 @@ export const event: Omit<OntimeEvent, 'id' | 'cue'> = {
timeStart: 0,
timeEnd: 0,
duration: 0,
isPublic: false,
skip: false,
colour: '',
timeWarning: 120000,
@@ -9,49 +9,41 @@ describe('loadRoll()', () => {
id: '1',
timeStart: 5,
timeEnd: 10,
isPublic: false,
},
{
id: '2',
timeStart: 10,
timeEnd: 20,
isPublic: false,
},
{
id: '3',
timeStart: 20,
timeEnd: 30,
isPublic: false,
},
{
id: '4',
timeStart: 30,
timeEnd: 40,
isPublic: false,
},
{
id: '5',
timeStart: 40,
timeEnd: 50,
isPublic: true,
},
{
id: '6',
timeStart: 50,
timeEnd: 60,
isPublic: false,
},
{
id: '7',
timeStart: 60,
timeEnd: 70,
isPublic: true,
},
{
id: '8',
timeStart: 70,
timeEnd: 80,
isPublic: false,
},
];
const timedEvents = prepareTimedEvents(eventlist);
@@ -155,31 +147,26 @@ describe('loadRoll() handle edge cases with midnight', () => {
id: '0',
timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: 10 * MILLIS_PER_HOUR,
isPublic: true,
},
{
id: '1',
timeStart: 20 * MILLIS_PER_HOUR,
timeEnd: 22 * MILLIS_PER_HOUR,
isPublic: true,
},
{
id: '2',
timeStart: 22 * MILLIS_PER_HOUR,
timeEnd: 1 * MILLIS_PER_HOUR,
isPublic: true,
},
{
id: '3',
timeStart: 1 * MILLIS_PER_HOUR,
timeEnd: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE,
isPublic: true,
},
{
id: '4',
timeStart: 1 * MILLIS_PER_HOUR,
timeEnd: 2 * MILLIS_PER_HOUR,
isPublic: true,
},
];
const timedEvents = prepareTimedEvents(eventlist);
@@ -311,7 +298,6 @@ describe('loadRoll() handle edge cases with before and after start', () => {
id: '1',
timeStart: 10 * MILLIS_PER_HOUR,
timeEnd: 11 * MILLIS_PER_HOUR,
isPublic: true,
}),
];
@@ -331,7 +317,6 @@ describe('loadRoll() handle edge cases with before and after start', () => {
id: '1',
timeStart: 10 * MILLIS_PER_HOUR,
timeEnd: 11 * MILLIS_PER_HOUR,
isPublic: true,
}),
];
@@ -351,7 +336,6 @@ describe('loadRoll() handle edge cases with before and after start', () => {
id: '1',
timeStart: 10 * MILLIS_PER_HOUR,
timeEnd: 2 * MILLIS_PER_HOUR,
isPublic: true,
}),
];
const expected = {
@@ -370,7 +354,6 @@ describe('loadRoll() handle edge cases with before and after start', () => {
id: '1',
timeStart: 72000000, // 20:00
timeEnd: 72010000, // 20:10
isPublic: true,
}),
];
const expected = {
@@ -389,19 +372,16 @@ describe('loadRoll() test that roll behaviour with overlapping times', () => {
id: '1',
timeStart: 10,
timeEnd: 10,
isPublic: false,
},
{
id: '2',
timeStart: 10,
timeEnd: 20,
isPublic: true,
},
{
id: '3',
timeStart: 10,
timeEnd: 30,
isPublic: false,
},
];
const timedEvents = prepareTimedEvents(eventlist);
@@ -472,7 +452,6 @@ describe('loadRoll() test that roll behaviour multi day event edge cases', () =>
id: '1',
timeStart: 66000000, // 19:20
timeEnd: 54600000, // 16:10
isPublic: false,
}),
];
const expected = {
@@ -491,7 +470,6 @@ describe('loadRoll() test that roll behaviour multi day event edge cases', () =>
id: '1',
timeStart: 67200000, // 19:40
timeEnd: 66900000, // 19:35
isPublic: false,
}),
];
const expected = {
@@ -914,7 +914,6 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
isPublic: true,
skip: false,
note: '',
colour: '',
@@ -967,7 +966,6 @@ describe('getRuntimeOffset()', () => {
endAction: EndAction.None,
timerType: TimerType.CountDown,
countToEnd: true,
isPublic: true,
skip: false,
note: '',
colour: '',
@@ -1222,9 +1220,7 @@ describe('getTimerPhase()', () => {
const state = {
clock: 55691050,
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 1,
@@ -1263,9 +1259,7 @@ describe('getTimerPhase()', () => {
const state = {
clock: 55691050,
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 1,
@@ -40,7 +40,7 @@ import {
getShouldTimerUpdate,
} from './rundownService.utils.js';
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow' | 'publicEventNow' | 'publicEventNext'>;
type RuntimeStateEventKeys = keyof Pick<RuntimeState, 'eventNext' | 'eventNow'>;
/**
* Service manages runtime status of app
@@ -185,15 +185,8 @@ class RuntimeService {
private affectsLoaded(affectedIds: string[]): boolean {
const state = runtimeState.getState();
const now = state.eventNow?.id;
const nowPublic = state.publicEventNow?.id;
const next = state.eventNext?.id;
const nextPublic = state.publicEventNext?.id;
return (
(now !== undefined && affectedIds.includes(now)) ||
(nowPublic !== undefined && affectedIds.includes(nowPublic)) ||
(next !== undefined && affectedIds.includes(next)) ||
(nextPublic !== undefined && affectedIds.includes(nextPublic))
);
return (now !== undefined && affectedIds.includes(now)) || (next !== undefined && affectedIds.includes(next));
}
private isNewNext() {
@@ -209,32 +202,7 @@ class RuntimeService {
const indexNow = timedEvents.findIndex((event) => event.id === now);
const indexNext = timedEvents.findIndex((event) => event.id === next);
if (indexNext - indexNow !== 1) {
return true;
}
// iterate through timed events and see if there are public events between nowPublic and nextPublic
const nowPublic = state.publicEventNow?.id;
const nextPublic = state.publicEventNext?.id;
let foundNew = false;
let isAfter = false;
for (const event of timedEvents) {
if (!isAfter) {
if (event.id === nowPublic) {
isAfter = true;
}
} else {
if (event.id === nextPublic) {
break;
}
if (event.isPublic) {
foundNew = true;
break;
}
}
}
return foundNew;
return indexNext - indexNow !== 1;
}
/**
@@ -852,9 +820,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
// Update the events if they have changed
updateEventIfChanged('eventNow', state);
updateEventIfChanged('publicEventNow', state);
updateEventIfChanged('eventNext', state);
updateEventIfChanged('publicEventNext', state);
// Helper function to update an event if it has changed
function updateEventIfChanged(eventKey: RuntimeStateEventKeys, state: runtimeState.RuntimeState) {
@@ -33,7 +33,6 @@ describe('cellRequestFromEvent()', () => {
timerType: TimerType.CountDown,
countToEnd: false,
duration: 10800000,
isPublic: false,
skip: false,
colour: 'red',
delay: 0,
@@ -57,7 +56,6 @@ describe('cellRequestFromEvent()', () => {
endAction: { row: 1, col: 22 },
timerType: { row: 1, col: 23 },
duration: { row: 1, col: 24 },
isPublic: { row: 1, col: 25 },
skip: { row: 1, col: 26 },
colour: { row: 1, col: 27 },
revision: { row: 1, col: 38 },
@@ -83,7 +81,6 @@ describe('cellRequestFromEvent()', () => {
duration: 10800000,
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
isPublic: false,
skip: false,
colour: 'red',
parent: null,
@@ -107,7 +104,6 @@ describe('cellRequestFromEvent()', () => {
endAction: { row: 1, col: 22 },
timerType: { row: 1, col: 23 },
duration: { row: 1, col: 24 },
isPublic: { row: 1, col: 25 },
skip: { row: 1, col: 26 },
colour: { row: 1, col: 27 },
revision: { row: 1, col: 38 },
@@ -135,7 +131,6 @@ describe('cellRequestFromEvent()', () => {
duration: 10800000,
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
isPublic: true,
skip: false,
colour: 'red',
parent: null,
@@ -159,7 +154,6 @@ describe('cellRequestFromEvent()', () => {
endAction: { row: 1, col: 22 },
timerType: { row: 1, col: 23 },
duration: { row: 1, col: 24 },
isPublic: { row: 1, col: 25 },
skip: { row: 1, col: 26 },
colour: { row: 1, col: 27 },
revision: { row: 1, col: 38 },
@@ -168,7 +162,6 @@ describe('cellRequestFromEvent()', () => {
timeDanger: { row: 1, col: 41 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata);
expect(result.updateCells?.rows?.at(0)?.values?.at(11)?.userEnteredValue?.boolValue).toStrictEqual(true);
expect(result.updateCells?.rows?.at(0)?.values?.at(12)?.userEnteredValue?.boolValue).toStrictEqual(false);
});
@@ -186,7 +179,6 @@ describe('cellRequestFromEvent()', () => {
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
duration: 10800000,
isPublic: true,
skip: false,
colour: 'red',
delay: 0,
@@ -223,7 +215,6 @@ describe('cellRequestFromEvent()', () => {
duration: 10800000,
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
isPublic: true,
skip: false,
colour: 'red',
parent: null,
@@ -261,7 +252,6 @@ describe('cellRequestFromEvent()', () => {
duration: 10800000,
timeStrategy: TimeStrategy.LockEnd,
linkStart: false,
isPublic: true,
skip: false,
colour: 'red',
parent: null,
@@ -9,9 +9,7 @@ const baseState: RuntimeState = {
startedAt: null,
},
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 0,
@@ -30,9 +30,7 @@ const mockEvent = {
const mockState = {
clock: 666,
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: {
selectedEventIndex: null,
numEvents: 0,
+3 -54
View File
@@ -34,9 +34,7 @@ export type RuntimeState = {
clock: number; // realtime clock
eventNow: PlayableEvent | null;
currentBlock: CurrentBlockState;
publicEventNow: PlayableEvent | null;
eventNext: PlayableEvent | null;
publicEventNext: PlayableEvent | null;
runtime: Runtime;
timer: TimerState;
// private properties of the timer calculations
@@ -54,9 +52,7 @@ const runtimeState: RuntimeState = {
clock: timeNow(),
currentBlock: { ...runtimeStorePlaceholder.currentBlock },
eventNow: null,
publicEventNow: null,
eventNext: null,
publicEventNext: null,
runtime: { ...runtimeStorePlaceholder.runtime },
timer: { ...runtimeStorePlaceholder.timer },
_timer: {
@@ -75,8 +71,6 @@ export function getState(): Readonly<RuntimeState> {
...runtimeState,
eventNow: runtimeState.eventNow ? { ...runtimeState.eventNow } : null,
eventNext: runtimeState.eventNext ? { ...runtimeState.eventNext } : null,
publicEventNow: runtimeState.publicEventNow ? { ...runtimeState.publicEventNow } : null,
publicEventNext: runtimeState.publicEventNext ? { ...runtimeState.publicEventNext } : null,
runtime: { ...runtimeState.runtime },
timer: { ...runtimeState.timer },
_timer: { ...runtimeState._timer },
@@ -89,9 +83,7 @@ export function getState(): Readonly<RuntimeState> {
*/
export function clearEventData() {
runtimeState.eventNow = null;
runtimeState.publicEventNow = null;
runtimeState.eventNext = null;
runtimeState.publicEventNext = null;
runtimeState.runtime.offset = 0;
runtimeState.runtime.relativeOffset = 0;
@@ -111,14 +103,11 @@ export function clearEventData() {
// clear all necessary data when doing a full stop and the event is unloaded
export function clearState() {
runtimeState.eventNow = null;
runtimeState.publicEventNow = null;
runtimeState.eventNext = null;
runtimeState.currentBlock.block = null;
runtimeState.currentBlock.startedAt = null;
runtimeState.publicEventNext = null;
runtimeState.runtime.offset = 0;
runtimeState.runtime.relativeOffset = 0;
runtimeState.runtime.actualStart = null;
@@ -242,33 +231,6 @@ export function loadNow(timedEvents: OntimeEvent[], eventIndex: MaybeNumber = ru
const event = timedEvents[eventIndex] as PlayableEvent;
runtimeState.runtime.selectedEventIndex = eventIndex;
runtimeState.eventNow = event;
// check if current is also public
if (event.isPublic) {
runtimeState.publicEventNow = event;
} else {
// assume there is no public event
runtimeState.publicEventNow = null;
// if there is nothing before, return
if (!eventIndex) {
return;
}
// iterate backwards to find it
for (let i = eventIndex; i >= 0; i--) {
const event = timedEvents[i];
// we dont deal with events that are not playable
if (!isPlayableEvent(event)) {
continue;
}
if (event.isPublic) {
runtimeState.publicEventNow = event;
break;
}
}
}
}
/**
@@ -281,34 +243,21 @@ export function loadNext(
if (eventIndex === null) {
// reset the state to indicate there is no future event
runtimeState.eventNext = null;
runtimeState.publicEventNext = null;
return;
}
// temporarily reset this value to simplify loop logic
runtimeState.eventNext = null;
//TODO: do we already have a it as a list of not skipped events
for (let i = eventIndex + 1; i < timedEvents.length; i++) {
const event = timedEvents[i];
// we dont deal with events that are not playable
if (!isPlayableEvent(event)) {
continue;
}
// the private event is the one immediately after the current event
if (runtimeState.eventNext === null) {
runtimeState.eventNext = event;
}
// if event is public
if (event.isPublic) {
runtimeState.publicEventNext = event;
}
// Stop if both are set
if (runtimeState.eventNext !== null && runtimeState.publicEventNext !== null) {
return;
}
runtimeState.eventNext = event;
return;
}
}
-16
View File
@@ -36,7 +36,6 @@
"timeStart": 36000000,
"timeEnd": 37200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -65,7 +64,6 @@
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -94,7 +92,6 @@
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -123,7 +120,6 @@
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -152,7 +148,6 @@
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -196,7 +191,6 @@
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -225,7 +219,6 @@
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -254,7 +247,6 @@
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -283,7 +275,6 @@
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -312,7 +303,6 @@
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -357,7 +347,6 @@
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -386,7 +375,6 @@
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -415,7 +403,6 @@
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -444,7 +431,6 @@
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -466,8 +452,6 @@
"project": {
"title": "Eurovision Song Contest",
"description": "Turin 2022",
"publicUrl": "www.getontime.no",
"publicInfo": "Rehearsal Schedule - Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
"projectLogo": null,
-7
View File
@@ -58,13 +58,6 @@ test.describe('pages routes are available', () => {
await page.screenshot({ path: 'automated-screenshots/backstage.png' });
});
test('public', async ({ page }) => {
await page.goto('http://localhost:4001/public');
await expect(page).toHaveTitle(/ontime/);
await page.screenshot({ path: 'automated-screenshots/public.png' });
});
test('studio', async ({ page }) => {
await page.goto('http://localhost:4001/studio');
-6
View File
@@ -30,12 +30,6 @@ test.describe('test view navigation feature', () => {
page.locator('data-test-id=backstage-view');
await expect(page).toHaveURL('http://localhost:4001/backstage');
await page.getByRole('button', { name: 'toggle menu' }).click();
page.locator('data-test-id=navigation__menu');
await page.getByRole('link', { name: 'Public' }).click();
page.locator('data-test-id=public-view');
await expect(page).toHaveURL('http://localhost:4001/public');
await page.getByRole('button', { name: 'toggle menu' }).click();
page.locator('data-test-id=navigation__menu');
await page.getByRole('link', { name: 'Lower Thirds' }).click();
+1 -5
View File
@@ -67,7 +67,7 @@ test('delays are show correctly', async ({ page }) => {
await page.getByTestId('block__title').click();
await page.getByTestId('block__title').fill('test');
await page.getByTestId('block__title').press('Enter');
await expect(page.getByTestId('entry-1').locator('#block-status')).toHaveAttribute('data-ispublic', 'true');
await expect(page.getByTestId('entry-1').locator('#block-status')).toHaveAttribute('data-timerType', 'count-down');
// add a delay
await page.getByRole('button', { name: 'Delay' }).nth(0).click();
@@ -82,10 +82,6 @@ test('delays are show correctly', async ({ page }) => {
await page.goto('http://localhost:4001/cuesheet');
await page.getByRole('cell', { name: 'Delayed by 1 min' }).click();
// delay is NOT shown in the public view
await page.goto('http://localhost:4001/public');
await page.getByText('00:10→00:20').click();
// delay is shown in the backstage view
await page.goto('http://localhost:4001/backstage');
await page.getByText('00:11→00:21').click();
+2 -2
View File
@@ -20,8 +20,8 @@ test('CRUD operations on the rundown', async ({ page }) => {
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
expect(await page.getByTestId('entry-3').getByTestId('time-input-timeStart').inputValue()).toContain('00:30:00');
// test quick add options - event is public
// test quick add options
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
await expect(page.getByTestId('entry-4').locator('#block-status')).toHaveAttribute('data-ispublic', 'true');
await expect(page.getByTestId('entry-4').locator('#block-status')).toHaveAttribute('data-timerType', 'count-down');
});
-16
View File
@@ -54,7 +54,6 @@
"timeStart": 36000000,
"timeEnd": 37200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -83,7 +82,6 @@
"timeStart": 37500000,
"timeEnd": 38700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -112,7 +110,6 @@
"timeStart": 39000000,
"timeEnd": 40200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -141,7 +138,6 @@
"timeStart": 40500000,
"timeEnd": 41700000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -170,7 +166,6 @@
"timeStart": 42000000,
"timeEnd": 43200000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -214,7 +209,6 @@
"timeStart": 47100000,
"timeEnd": 48300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -243,7 +237,6 @@
"timeStart": 48600000,
"timeEnd": 49800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -272,7 +265,6 @@
"timeStart": 50100000,
"timeEnd": 51300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -301,7 +293,6 @@
"timeStart": 51600000,
"timeEnd": 52800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -330,7 +321,6 @@
"timeStart": 53100000,
"timeEnd": 54300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -374,7 +364,6 @@
"timeStart": 56100000,
"timeEnd": 57300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -403,7 +392,6 @@
"timeStart": 57600000,
"timeEnd": 58800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -432,7 +420,6 @@
"timeStart": 59100000,
"timeEnd": 60300000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -461,7 +448,6 @@
"timeStart": 60600000,
"timeEnd": 61800000,
"duration": 1200000,
"isPublic": true,
"skip": false,
"colour": "",
"parent": null,
@@ -483,8 +469,6 @@
"project": {
"title": "Eurovision Song Contest",
"description": "Turin 2022",
"publicUrl": "www.getontime.no",
"publicInfo": "Rehearsal Schedule - Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
"projectLogo": null,
@@ -48,7 +48,6 @@ export type OntimeEvent = OntimeBaseEvent & {
timeStart: number;
timeEnd: number;
duration: number;
isPublic: boolean;
skip: boolean;
colour: string;
timeWarning: number;
@@ -1,8 +1,6 @@
export type ProjectData = {
title: string;
description: string;
publicUrl: string;
publicInfo: string;
backstageUrl: string;
backstageInfo: string;
projectLogo: string | null;
@@ -46,8 +46,6 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
},
eventNow: null,
eventNext: null,
publicEventNow: null,
publicEventNext: null,
auxtimer1: {
current: 0,
direction: SimpleDirection.CountUp,
@@ -18,9 +18,7 @@ export type RuntimeStore = {
runtime: Runtime;
currentBlock: CurrentBlockState;
eventNow: OntimeEvent | null;
publicEventNow: OntimeEvent | null;
eventNext: OntimeEvent | null;
publicEventNext: OntimeEvent | null;
// extra timers
auxtimer1: SimpleTimerState;
@@ -12,7 +12,6 @@ describe('isImportMap()', () => {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
@@ -37,7 +36,6 @@ describe('isImportMap()', () => {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
@@ -61,7 +59,6 @@ describe('isImportMap()', () => {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',
@@ -12,7 +12,6 @@ export const defaultImportMap = {
cue: 'cue',
title: 'title',
countToEnd: 'count to end',
isPublic: 'public',
skip: 'skip',
note: 'notes',
colour: 'colour',