mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 23:18:01 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9813643c96 | |||
| b88c2a6bd8 |
@@ -13,10 +13,10 @@
|
||||
"@fontsource/open-sans": "^5.0.28",
|
||||
"@mantine/hooks": "^7.13.3",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@sentry/react": "^8.43.0",
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@tanstack/react-query-devtools": "^5.62.7",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@sentry/react": "^8.19.0",
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
"@tanstack/react-query-devtools": "^5.17.9",
|
||||
"@tanstack/react-table": "^8.11.3",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.2.0",
|
||||
"color": "^4.2.3",
|
||||
@@ -92,4 +92,4 @@
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
||||
/**
|
||||
* HTTP request to post new event
|
||||
*/
|
||||
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
export async function requestPostEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||
return axios.post(rundownPath, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.drawerFooter {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: end;
|
||||
gap: $section-spacing;
|
||||
|
||||
button {
|
||||
@@ -8,23 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $element-spacing;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
background-color: $gray-1100;
|
||||
border-radius: 2px;
|
||||
font-size: $inner-section-text-size;
|
||||
|
||||
svg {
|
||||
font-size: 1.5rem;
|
||||
color: $info-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $inner-section-text-size;
|
||||
color: $label-gray;
|
||||
|
||||
@@ -11,9 +11,6 @@ import {
|
||||
DrawerOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoAlertCircle } from '@react-icons/all-files/io5/IoAlertCircle';
|
||||
|
||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||
|
||||
import ParamInput from './ParamInput';
|
||||
import { isSection, ViewOption } from './types';
|
||||
@@ -90,8 +87,6 @@ interface EditFormDrawerProps {
|
||||
// TODO: this is a good candidate for memoisation, but needs the paramFields to be stable
|
||||
export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { data: viewSettings } = useViewSettings();
|
||||
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -132,12 +127,6 @@ export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
{viewSettings.overrideStyles && (
|
||||
<div className={style.infoLabel}>
|
||||
<IoAlertCircle />
|
||||
This view style is being modified by a custom CSS file. <br />
|
||||
</div>
|
||||
)}
|
||||
<form id='edit-params-form' onSubmit={onParamsFormSubmit}>
|
||||
{viewOptions.map((option) => {
|
||||
if (isSection(option)) {
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
isOntimeEvent,
|
||||
OntimeBlock,
|
||||
OntimeDelay,
|
||||
OntimeEvent,
|
||||
OntimeRundownEntry,
|
||||
RundownCached,
|
||||
TransientEventPayload,
|
||||
} from 'ontime-types';
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
@@ -27,16 +19,6 @@ import {
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { useEditorSettings } from '../stores/editorSettings';
|
||||
|
||||
export type EventOptions = Partial<{
|
||||
// options to any new block (event / delay / block)
|
||||
after: string;
|
||||
before: string;
|
||||
// options to blocks of type OntimeEvent
|
||||
defaultPublic: boolean;
|
||||
linkPrevious: boolean;
|
||||
lastEventId: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @description Set of utilities for events //TODO: should this be called useEntryAction and so on
|
||||
*/
|
||||
@@ -65,19 +47,31 @@ export const useEventAction = () => {
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
// options to any new block (event / delay / block)
|
||||
type BaseOptions = {
|
||||
after?: string;
|
||||
};
|
||||
|
||||
// options to blocks of type OntimeEvent
|
||||
type EventOptions = BaseOptions &
|
||||
Partial<{
|
||||
defaultPublic: boolean;
|
||||
linkPrevious: boolean;
|
||||
lastEventId: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Adds an event to rundown
|
||||
*/
|
||||
const addEvent = useCallback(
|
||||
async (event: Partial<OntimeEvent | OntimeDelay | OntimeBlock>, options?: EventOptions) => {
|
||||
const newEvent: TransientEventPayload = { ...event };
|
||||
async (event: Partial<OntimeRundownEntry>, options?: EventOptions) => {
|
||||
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
||||
|
||||
// ************* CHECK OPTIONS specific to events
|
||||
if (isOntimeEvent(newEvent)) {
|
||||
// merge creation time options with event settings
|
||||
const applicationOptions = {
|
||||
after: options?.after,
|
||||
before: options?.before,
|
||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||
lastEventId: options?.lastEventId,
|
||||
linkPrevious: options?.linkPrevious ?? linkPrevious,
|
||||
@@ -127,16 +121,11 @@ export const useEventAction = () => {
|
||||
|
||||
// handle adding options that concern all event type
|
||||
if (options?.after) {
|
||||
// @ts-expect-error -- not sure how to type this, <after> is a transient property
|
||||
newEvent.after = options.after;
|
||||
}
|
||||
if (options?.before) {
|
||||
// @ts-expect-error -- not sure how to type this, <before> is a transient property
|
||||
newEvent.before = options.before;
|
||||
}
|
||||
|
||||
try {
|
||||
await _addEventMutation.mutateAsync(newEvent as TransientEventPayload);
|
||||
await _addEventMutation.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
logAxiosError('Failed adding event', error);
|
||||
}
|
||||
|
||||
@@ -149,18 +149,16 @@ export const setAuxTimer = {
|
||||
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
||||
};
|
||||
|
||||
export const useSelectedEventId = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
export const useCurrentBlockId = () => {
|
||||
export const useCuesheet = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
currentBlockId: state.currentBlock.block?.id ?? null,
|
||||
selectedEventId: state.eventNow?.id ?? null,
|
||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
||||
numEvents: state.runtime.numEvents,
|
||||
titleNow: state.eventNow?.title || '',
|
||||
});
|
||||
|
||||
return useRuntimeStore(featureSelector);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
* @return {OntimeEvent} clean event
|
||||
*/
|
||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
||||
export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||
return {
|
||||
type: SupportedEvent.Event,
|
||||
title: event.title,
|
||||
@@ -23,6 +23,7 @@ export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
after,
|
||||
revision: 0,
|
||||
timeWarning: event.timeWarning,
|
||||
timeDanger: event.timeDanger,
|
||||
|
||||
@@ -16,8 +16,4 @@
|
||||
&.finished {
|
||||
color: $timer-finished-color;
|
||||
}
|
||||
|
||||
&.muted {
|
||||
color: $muted-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function TimerDisplay(props: TimerDisplayProps) {
|
||||
const isNegative = (time ?? 0) < 0;
|
||||
const display =
|
||||
time == null ? timerPlaceholder : millisToString(time, { fallback: timerPlaceholder }).replace('-', '');
|
||||
const classes = cx([style.timer, isNegative ? style.finished : null, time === null && style.muted]);
|
||||
const classes = cx([style.timer, isNegative ? style.finished : null]);
|
||||
|
||||
return <div className={classes}>{display}</div>;
|
||||
}
|
||||
|
||||
@@ -4,23 +4,6 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.isOffline {
|
||||
.info {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
&::after {
|
||||
content: 'Disconnected';
|
||||
position: absolute;
|
||||
padding-inline: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background-color: $red-700;
|
||||
border-radius: 2px;
|
||||
font-size: calc(1rem - 2px);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { memo, PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import { useIsOnline, useRuntimeOverview, useRuntimePlaybackOverview, useTimer } from '../../common/hooks/useSocket';
|
||||
import { useRuntimeOverview, useRuntimePlaybackOverview, useTimer } from '../../common/hooks/useSocket';
|
||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||
import { cx, enDash, timerPlaceholder } from '../../common/utils/styleUtils';
|
||||
import { enDash } from '../../common/utils/styleUtils';
|
||||
|
||||
import { TimeColumn, TimeRow } from './composite/TimeLayout';
|
||||
import { calculateEndAndDaySpan, formatedTime, getOffsetText } from './overviewUtils';
|
||||
@@ -13,7 +13,7 @@ import style from './Overview.module.scss';
|
||||
|
||||
export const EditorOverview = memo(_EditorOverview);
|
||||
|
||||
function _EditorOverview({ children }: PropsWithChildren) {
|
||||
function _EditorOverview({ children }: { children: React.ReactNode }) {
|
||||
const { plannedEnd, plannedStart, actualStart, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||
@@ -23,48 +23,36 @@ function _EditorOverview({ children }: PropsWithChildren) {
|
||||
const expectedEndText = formatedTime(maybeExpectedEnd);
|
||||
|
||||
return (
|
||||
<OverviewWrapper navElements={children}>
|
||||
<TitlesOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned start'
|
||||
value={formatedTime(plannedStart)}
|
||||
className={style.start}
|
||||
muted={plannedStart === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Actual start'
|
||||
value={formatedTime(actualStart)}
|
||||
className={style.start}
|
||||
muted={actualStart === null}
|
||||
/>
|
||||
</div>
|
||||
<ProgressOverview />
|
||||
<CurrentBlockOverview />
|
||||
<RuntimeOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned end'
|
||||
value={plannedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybePlannedDaySpan}
|
||||
muted={maybePlannedEnd === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
muted={maybeExpectedEnd === null}
|
||||
/>
|
||||
</div>
|
||||
</OverviewWrapper>
|
||||
<div className={style.overview}>
|
||||
<ErrorBoundary>
|
||||
<div className={style.nav}>{children}</div>
|
||||
<div className={style.info}>
|
||||
<TitlesOverview />
|
||||
<div>
|
||||
<TimeRow label='Planned start' value={formatedTime(plannedStart)} className={style.start} />
|
||||
<TimeRow label='Actual start' value={formatedTime(actualStart)} className={style.start} />
|
||||
</div>
|
||||
<ProgressOverview />
|
||||
<CurrentBlockOverview />
|
||||
<RuntimeOverview />
|
||||
<div>
|
||||
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CuesheetOverview = memo(_CuesheetOverview);
|
||||
|
||||
function _CuesheetOverview({ children }: PropsWithChildren) {
|
||||
function _CuesheetOverview({ children }: { children: React.ReactNode }) {
|
||||
const { plannedEnd, expectedEnd } = useRuntimeOverview();
|
||||
|
||||
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
|
||||
@@ -74,41 +62,23 @@ function _CuesheetOverview({ children }: PropsWithChildren) {
|
||||
const expectedEndText = formatedTime(maybeExpectedEnd);
|
||||
|
||||
return (
|
||||
<OverviewWrapper navElements={children}>
|
||||
<TitlesOverview />
|
||||
<TimerOverview />
|
||||
<RuntimeOverview />
|
||||
<div>
|
||||
<TimeRow
|
||||
label='Planned end'
|
||||
value={plannedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybePlannedDaySpan}
|
||||
muted={maybePlannedEnd === null}
|
||||
/>
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
muted={maybeExpectedEnd === null}
|
||||
/>
|
||||
</div>
|
||||
</OverviewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
interface OverviewWrapperProps {
|
||||
navElements: ReactNode;
|
||||
}
|
||||
|
||||
function OverviewWrapper({ navElements, children }: PropsWithChildren<OverviewWrapperProps>) {
|
||||
const { isOnline } = useIsOnline();
|
||||
return (
|
||||
<div className={cx([style.overview, !isOnline && style.isOffline])}>
|
||||
<div className={style.overview}>
|
||||
<ErrorBoundary>
|
||||
<div className={style.nav}>{navElements}</div>
|
||||
<div className={style.info}>{children}</div>
|
||||
<div className={style.nav}>{children}</div>
|
||||
<div className={style.info}>
|
||||
<TitlesOverview />
|
||||
<TimerOverview />
|
||||
<RuntimeOverview />
|
||||
<div>
|
||||
<TimeRow label='Planned end' value={plannedEndText} className={style.end} daySpan={maybePlannedDaySpan} />
|
||||
<TimeRow
|
||||
label='Expected end'
|
||||
value={expectedEndText}
|
||||
className={style.end}
|
||||
daySpan={maybeExpectedDaySpan}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
@@ -134,22 +104,15 @@ function CurrentBlockOverview() {
|
||||
|
||||
const timeInBlock = formatedTime(currentBlock.startedAt === null ? null : clock - currentBlock.startedAt);
|
||||
|
||||
return (
|
||||
<TimeColumn
|
||||
label='Time in block'
|
||||
value={timeInBlock}
|
||||
className={style.clock}
|
||||
muted={currentBlock.startedAt === null}
|
||||
/>
|
||||
);
|
||||
return <TimeColumn label='Time in block' value={timeInBlock} className={style.clock} />;
|
||||
}
|
||||
|
||||
function TimerOverview() {
|
||||
const { current } = useTimer();
|
||||
|
||||
const display = millisToString(current, { fallback: timerPlaceholder });
|
||||
const display = millisToString(current);
|
||||
|
||||
return <TimeColumn label='Running timer' value={display} muted={current === null} />;
|
||||
return <TimeColumn label='Running timer' value={display} />;
|
||||
}
|
||||
|
||||
function ProgressOverview() {
|
||||
|
||||
@@ -44,10 +44,6 @@
|
||||
content: "*";
|
||||
vertical-align: super;
|
||||
font-size: 0.75em;
|
||||
color: $info-blue;
|
||||
color: $blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: $muted-gray;
|
||||
}
|
||||
|
||||
@@ -7,21 +7,20 @@ import style from './TimeLayout.module.scss';
|
||||
interface TimeLayoutProps {
|
||||
label: string;
|
||||
value: string;
|
||||
muted?: boolean;
|
||||
daySpan?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TimeColumn({ label, value, muted, className }: TimeLayoutProps) {
|
||||
export function TimeColumn({ label, value, className }: TimeLayoutProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
|
||||
<span className={cx([style.clock, className])}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutProps) {
|
||||
export function TimeRow({ label, value, daySpan, className }: TimeLayoutProps) {
|
||||
return (
|
||||
<div className={style.row}>
|
||||
<span className={style.label}>{label}</span>
|
||||
@@ -30,7 +29,7 @@ export function TimeRow({ label, value, daySpan, muted, className }: TimeLayoutP
|
||||
<span className={cx([style.clock, style.daySpan, className])}>{value}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={cx([style.clock, muted && style.muted, className])}>{value}</span>
|
||||
<span className={cx([style.clock, className])}>{value}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeString,
|
||||
PlayableEvent,
|
||||
Playback,
|
||||
RundownCached,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
isNewLatest,
|
||||
} from 'ontime-utils';
|
||||
|
||||
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||
@@ -83,36 +82,36 @@ export default function Rundown({ data }: RundownProps) {
|
||||
const cloneEntry = rundown[copyId];
|
||||
if (cloneEntry?.type === SupportedEvent.Event) {
|
||||
//if we don't have a cursor add the new event on top
|
||||
const newEvent = cloneEvent(cloneEntry);
|
||||
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
||||
const newEvent = cloneEvent(cloneEntry, adjustedCursor ?? undefined);
|
||||
addEvent(newEvent);
|
||||
}
|
||||
},
|
||||
[addEvent, order, rundown],
|
||||
);
|
||||
|
||||
const insertAtId = useCallback(
|
||||
(type: SupportedEvent, id: MaybeString, above = false) => {
|
||||
const options: EventOptions =
|
||||
id === null
|
||||
? {}
|
||||
: {
|
||||
after: above ? undefined : id,
|
||||
before: above ? id : undefined,
|
||||
};
|
||||
(type: SupportedEvent, id: string | null, above = false) => {
|
||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, id ?? '').entry?.id ?? null : id;
|
||||
if (adjustedCursor === null) {
|
||||
// the only thing to do is adding an event at top
|
||||
addEvent({ type });
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === SupportedEvent.Event) {
|
||||
const newEvent = {
|
||||
type: SupportedEvent.Event,
|
||||
};
|
||||
if (!above && id) {
|
||||
options.lastEventId = id;
|
||||
}
|
||||
const options = {
|
||||
after: adjustedCursor,
|
||||
lastEventId: adjustedCursor,
|
||||
};
|
||||
addEvent(newEvent, options);
|
||||
} else {
|
||||
addEvent({ type }, options);
|
||||
addEvent({ type }, { after: adjustedCursor });
|
||||
}
|
||||
},
|
||||
[addEvent],
|
||||
[rundown, order, addEvent],
|
||||
);
|
||||
|
||||
const selectBlock = useCallback(
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
return deleteEvent([data.id]);
|
||||
}
|
||||
case 'clone': {
|
||||
const newEvent = cloneEvent(data as OntimeEvent);
|
||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||
addEvent(newEvent, { after: data.id });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { handleLinks } from '../../common/utils/linkUtils';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { Corner } from '../editors/editor-utils/EditorUtils';
|
||||
|
||||
import RundownEventEditor from './event-editor/RundownEventEditor';
|
||||
import EventEditor from './event-editor/EventEditor';
|
||||
import RundownWrapper from './RundownWrapper';
|
||||
|
||||
import style from './RundownExport.module.scss';
|
||||
@@ -33,7 +33,7 @@ const RundownExport = () => {
|
||||
{!hideSideBar && (
|
||||
<div className={style.side}>
|
||||
<ErrorBoundary>
|
||||
<RundownEventEditor />
|
||||
<EventEditor />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
|
||||
import EventEditor from './EventEditor';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
interface CuesheetEventEditorProps {
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
|
||||
const { eventId } = props;
|
||||
const { data } = useRundown();
|
||||
const { order, rundown } = data;
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = rundown[eventId];
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [data, eventId, order, rundown]);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.eventEditor} data-testid='editor-container'>
|
||||
<EventEditor event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,14 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid $white-10;
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.timeSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { CSSProperties, useCallback } from 'react';
|
||||
import { CSSProperties, memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { CustomFieldLabel, OntimeEvent } from 'ontime-types';
|
||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import CopyTag from '../../../common/components/copy-tag/CopyTag';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
@@ -19,17 +22,35 @@ export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
||||
|
||||
interface EventEditorProps {
|
||||
event: OntimeEvent;
|
||||
}
|
||||
|
||||
export default function EventEditor(props: EventEditorProps) {
|
||||
const { event } = props;
|
||||
export default function EventEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { order, rundown } = data;
|
||||
const { updateEvent } = useEventAction();
|
||||
const [_searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
const event = rundown[selectedEventId];
|
||||
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [order, rundown, selectedEvents]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: EditorUpdateFields, value: string) => {
|
||||
@@ -52,61 +73,87 @@ export default function EventEditor(props: EventEditorProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<EventEditorTimes
|
||||
key={`${event.id}-times`}
|
||||
eventId={event.id}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
duration={event.duration}
|
||||
timeStrategy={event.timeStrategy}
|
||||
linkStart={event.linkStart}
|
||||
isTimeToEnd={event.isTimeToEnd}
|
||||
delay={event.delay ?? 0}
|
||||
isPublic={event.isPublic}
|
||||
endAction={event.endAction}
|
||||
timerType={event.timerType}
|
||||
timeWarning={event.timeWarning}
|
||||
timeDanger={event.timeDanger}
|
||||
/>
|
||||
<EventEditorTitles
|
||||
key={`${event.id}-titles`}
|
||||
eventId={event.id}
|
||||
cue={event.cue}
|
||||
title={event.title}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<div className={style.column}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Editor.Title>Custom Fields</Editor.Title>
|
||||
{isEditor && (
|
||||
<div className={style.eventEditor} data-testid='editor-container'>
|
||||
<div className={style.content}>
|
||||
<EventEditorTimes
|
||||
key={`${event.id}-times`}
|
||||
eventId={event.id}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
duration={event.duration}
|
||||
timeStrategy={event.timeStrategy}
|
||||
linkStart={event.linkStart}
|
||||
isTimeToEnd={event.isTimeToEnd}
|
||||
delay={event.delay ?? 0}
|
||||
isPublic={event.isPublic}
|
||||
endAction={event.endAction}
|
||||
timerType={event.timerType}
|
||||
timeWarning={event.timeWarning}
|
||||
timeDanger={event.timeDanger}
|
||||
/>
|
||||
<EventEditorTitles
|
||||
key={`${event.id}-titles`}
|
||||
eventId={event.id}
|
||||
cue={event.cue}
|
||||
title={event.title}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<div className={style.column}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Editor.Title>Custom Fields</Editor.Title>
|
||||
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{Object.keys(customFields).map((fieldKey) => {
|
||||
const key = `${event.id}-${fieldKey}`;
|
||||
const fieldName = `custom-${fieldKey}`;
|
||||
const initialValue = event.custom[fieldKey] ?? '';
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
|
||||
const labelText = customFields[fieldKey].label;
|
||||
</div>
|
||||
{Object.keys(customFields).map((fieldKey) => {
|
||||
const key = `${event.id}-${fieldKey}`;
|
||||
const fieldName = `custom-${fieldKey}`;
|
||||
const initialValue = event.custom[fieldKey] ?? '';
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
|
||||
const labelText = customFields[fieldKey].label;
|
||||
|
||||
return (
|
||||
<EventTextArea
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<EventTextArea
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<EventEditorFooter id={event.id} cue={event.cue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventEditorFooterProps {
|
||||
id: string;
|
||||
cue: string;
|
||||
}
|
||||
|
||||
const EventEditorFooter = memo(_EventEditorFooter);
|
||||
|
||||
function _EventEditorFooter(props: EventEditorFooterProps) {
|
||||
const { id, cue } = props;
|
||||
|
||||
const loadById = `/ontime/load/id "${id}"`;
|
||||
const loadByCue = `/ontime/load/cue "${cue}"`;
|
||||
|
||||
return (
|
||||
<div className={style.footer}>
|
||||
<CopyTag copyValue={loadById} label='OSC trigger by ID'>
|
||||
{loadById}
|
||||
</CopyTag>
|
||||
<CopyTag copyValue={loadByCue} label='OSC trigger by cue'>
|
||||
{loadByCue}
|
||||
</CopyTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import { EventEditorFooter } from './composite/EventEditorFooter';
|
||||
import EventEditor from './EventEditor';
|
||||
import EventEditorEmpty from './EventEditorEmpty';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
export default function RundownEventEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
const { order, rundown } = data;
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = order.find((eventId) => selectedEvents.has(eventId));
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
const event = rundown[selectedEventId];
|
||||
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [order, rundown, selectedEvents]);
|
||||
|
||||
if (!event) {
|
||||
return <EventEditorEmpty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.eventEditor} data-testid='editor-container'>
|
||||
<EventEditor event={event} />
|
||||
<EventEditorFooter id={event.id} cue={event.cue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.footer {
|
||||
border-top: 1px solid $white-10;
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||
|
||||
import style from './EventEditorFooter.module.scss';
|
||||
|
||||
interface EventEditorFooterProps {
|
||||
id: string;
|
||||
cue: string;
|
||||
}
|
||||
|
||||
export const EventEditorFooter = memo(_EventEditorFooter);
|
||||
|
||||
function _EventEditorFooter(props: EventEditorFooterProps) {
|
||||
const { id, cue } = props;
|
||||
|
||||
const loadById = `/ontime/load/id "${id}"`;
|
||||
const loadByCue = `/ontime/load/cue "${cue}"`;
|
||||
|
||||
return (
|
||||
<div className={style.footer}>
|
||||
<CopyTag copyValue={loadById} label='OSC trigger by ID'>
|
||||
{loadById}
|
||||
</CopyTag>
|
||||
<CopyTag copyValue={loadByCue} label='OSC trigger by cue'>
|
||||
{loadByCue}
|
||||
</CopyTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,10 +38,6 @@ export function getTimerByType(freezeEnd: boolean, timerObject?: TimerTypeParams
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string to semantically verify if it represents a true value
|
||||
* Used in the context of parsing search params and local storage items which can be strings or null
|
||||
*/
|
||||
export function isStringBoolean(text: string | null) {
|
||||
if (text === null) {
|
||||
return false;
|
||||
|
||||
@@ -12,7 +12,6 @@ $action-text-color: $blue-400;
|
||||
$ontime-color: #ff7597;
|
||||
$error-red: $red-500;
|
||||
$warning-orange: $orange-500;
|
||||
$info-blue: $blue-500;
|
||||
$opacity-disabled: 0.4;
|
||||
$active-red: $red-700;
|
||||
|
||||
@@ -52,7 +51,6 @@ $main-spacing: 2rem;
|
||||
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif;
|
||||
$label-gray: $gray-400;
|
||||
$secondary-text-gray: $gray-400;
|
||||
$muted-gray: $gray-600;
|
||||
$section-white: $ui-white;
|
||||
$inner-section-text-size: calc(1rem - 2px);
|
||||
$text-body-size: calc(1rem - 1px);
|
||||
|
||||
@@ -38,16 +38,6 @@ export const ontimeInputGhosted = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeInputTransparent = {
|
||||
field: {
|
||||
...commonStyles,
|
||||
backgroundColor: 'transparent',
|
||||
_hover: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.10)', // $white-10
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeTextAreaFilled = {
|
||||
...commonStyles,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ import { ontimeTab } from './ontimeTab';
|
||||
import {
|
||||
ontimeInputFilled,
|
||||
ontimeInputGhosted,
|
||||
ontimeInputTransparent,
|
||||
ontimeTextAreaFilled,
|
||||
ontimeTextAreaTransparent,
|
||||
} from './ontimeTextInputs';
|
||||
@@ -79,7 +78,6 @@ const theme = extendTheme({
|
||||
variants: {
|
||||
'ontime-filled': { ...ontimeInputFilled },
|
||||
'ontime-ghosted': { ...ontimeInputGhosted },
|
||||
'ontime-transparent': { ...ontimeInputTransparent },
|
||||
},
|
||||
},
|
||||
Kbd: {
|
||||
|
||||
+5
-15
@@ -1,5 +1,5 @@
|
||||
$table-font-size: 1rem;
|
||||
$table-header-font-size: calc(1rem - 2px);
|
||||
$table-font-size: calc(1rem - 2px);
|
||||
$table-header-font-size: calc(1rem - 3px);
|
||||
|
||||
.cuesheetContainer {
|
||||
grid-area: table;
|
||||
@@ -33,22 +33,13 @@ $table-header-font-size: calc(1rem - 2px);
|
||||
.tableHeader,
|
||||
.eventRow {
|
||||
.indexColumn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
min-width: 3em; // allow for 3-digit numbers
|
||||
min-width: 2rem;
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
font-size: $table-header-font-size;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: $gray-1300; // will be overridden inline
|
||||
}
|
||||
|
||||
.actionColumn {
|
||||
width: calc(1.5rem + 0.5rem); // sm button size (--chakra-sizes-6) + 2 * padding
|
||||
background-color: $gray-1300;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +49,7 @@ $table-header-font-size: calc(1rem - 2px);
|
||||
z-index: 10;
|
||||
background-color: $ui-black;
|
||||
font-size: $table-header-font-size;
|
||||
color: $label-gray;
|
||||
}
|
||||
color: $label-gray;}
|
||||
|
||||
th {
|
||||
background-color: $gray-1300;
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { getAccessibleColour } from '../../common/utils/styleUtils';
|
||||
|
||||
import BlockRow from './cuesheet-table-elements/BlockRow';
|
||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||
import EventRow from './cuesheet-table-elements/EventRow';
|
||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
|
||||
import useColumnManager from './useColumnManager';
|
||||
|
||||
import style from './Cuesheet.module.scss';
|
||||
|
||||
interface CuesheetProps {
|
||||
data: OntimeRundown;
|
||||
columns: ColumnDef<OntimeRundownEntry>[];
|
||||
handleUpdate: (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: unknown) => void;
|
||||
selectedId: string | null;
|
||||
currentBlockId: string | null;
|
||||
}
|
||||
|
||||
export default function Cuesheet({ data, columns, handleUpdate, selectedId, currentBlockId }: CuesheetProps) {
|
||||
const { followSelected, showSettings, showDelayBlock, showPrevious, showIndexColumn } = useCuesheetSettings();
|
||||
const {
|
||||
columnVisibility,
|
||||
columnOrder,
|
||||
columnSizing,
|
||||
resetColumnOrder,
|
||||
setColumnVisibility,
|
||||
saveColumnOrder,
|
||||
setColumnSizing,
|
||||
} = useColumnManager(columns);
|
||||
|
||||
const selectedRef = useRef<HTMLTableRowElement | null>(null);
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
columnResizeMode: 'onChange',
|
||||
state: {
|
||||
columnOrder,
|
||||
columnVisibility,
|
||||
columnSizing,
|
||||
},
|
||||
meta: {
|
||||
handleUpdate,
|
||||
},
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const setAllVisible = () => {
|
||||
table.toggleAllColumnsVisible(true);
|
||||
};
|
||||
|
||||
const resetColumnResizing = () => {
|
||||
setColumnSizing({});
|
||||
};
|
||||
|
||||
const reorder = useCallback(
|
||||
(fromId: string, toId: string) => {
|
||||
// get index of from
|
||||
const fromIndex = columnOrder.indexOf(fromId);
|
||||
|
||||
// get index of to
|
||||
const toIndex = columnOrder.indexOf(toId);
|
||||
|
||||
if (toIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedCols = [...columnOrder];
|
||||
const reorderedItem = reorderedCols.splice(fromIndex, 1);
|
||||
reorderedCols.splice(toIndex, 0, reorderedItem[0]);
|
||||
saveColumnOrder(reorderedCols);
|
||||
},
|
||||
[columnOrder, saveColumnOrder],
|
||||
);
|
||||
|
||||
const headerGroups = table.getHeaderGroups();
|
||||
const rowModel = table.getRowModel();
|
||||
const allLeafColumns = table.getAllLeafColumns();
|
||||
|
||||
let eventIndex = 0;
|
||||
let isPast = Boolean(selectedId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSettings && (
|
||||
<CuesheetTableSettings
|
||||
columns={allLeafColumns}
|
||||
handleResetResizing={resetColumnResizing}
|
||||
handleResetReordering={resetColumnOrder}
|
||||
handleClearToggles={setAllVisible}
|
||||
/>
|
||||
)}
|
||||
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
||||
<table className={style.cuesheet}>
|
||||
<CuesheetHeader headerGroups={headerGroups} saveColumnOrder={reorder} showIndexColumn={showIndexColumn} />
|
||||
<tbody>
|
||||
{rowModel.rows.map((row) => {
|
||||
const key = row.original.id;
|
||||
const isSelected = selectedId === key;
|
||||
if (isSelected) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(row.original)) {
|
||||
if (isPast && !showPrevious && key !== currentBlockId) {
|
||||
return null;
|
||||
}
|
||||
return <BlockRow key={key} title={row.original.title} />;
|
||||
}
|
||||
if (isOntimeDelay(row.original)) {
|
||||
if (isPast && !showPrevious) {
|
||||
return null;
|
||||
}
|
||||
const delayVal = row.original.duration;
|
||||
if (!showDelayBlock || delayVal === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DelayRow key={key} duration={delayVal} />;
|
||||
}
|
||||
if (isOntimeEvent(row.original)) {
|
||||
eventIndex++;
|
||||
const isSelected = key === selectedId;
|
||||
|
||||
if (isPast && !showPrevious) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (isSelected) {
|
||||
rowBgColour = '#D20300'; // $red-700
|
||||
} else if (row.original.colour) {
|
||||
try {
|
||||
// the colour is user defined and might be invalid
|
||||
const accessibleBackgroundColor = Color(getAccessibleColour(row.original.colour).backgroundColor);
|
||||
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
||||
} catch (_error) {
|
||||
/* we do not handle errors here */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EventRow
|
||||
key={key}
|
||||
eventIndex={eventIndex}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
skip={row.original.skip}
|
||||
colour={row.original.colour}
|
||||
showIndexColumn={showIndexColumn}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</EventRow>
|
||||
);
|
||||
}
|
||||
|
||||
// currently there is no scenario where entryType is not handled above, either way...
|
||||
return null;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,9 @@
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 3rem auto auto 1fr;
|
||||
grid-template-rows: 3rem auto 1fr;
|
||||
grid-template-areas:
|
||||
'overview'
|
||||
'progress'
|
||||
'settings'
|
||||
'table';
|
||||
gap: 1rem;
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { CustomFieldLabel, isOntimeEvent } from 'ontime-types';
|
||||
|
||||
import ProductionNavigationMenu from '../../common/components/navigation-menu/ProductionNavigationMenu';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||
import { CuesheetOverview } from '../../features/overview/Overview';
|
||||
import CuesheetEventEditor from '../../features/rundown/event-editor/CuesheetEventEditor';
|
||||
|
||||
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
||||
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols';
|
||||
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||
import { cuesheetOptions } from './cuesheet.options';
|
||||
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
|
||||
import Cuesheet from './Cuesheet';
|
||||
import { makeCuesheetColumns } from './cuesheetCols';
|
||||
|
||||
import styles from './CuesheetPage.module.scss';
|
||||
|
||||
@@ -27,56 +24,21 @@ export default function CuesheetPage() {
|
||||
// TODO: can we use the normalised rundown for the table?
|
||||
const { data: flatRundown, status: rundownStatus } = useFlatRundown();
|
||||
const { data: customFields } = useCustomFields();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: isEventEditorOpen, onOpen: onEventEditorOpen, onClose: onEventEditorClose } = useDisclosure();
|
||||
const [eventId, setEventId] = useState<string | null>(null);
|
||||
|
||||
const { updateCustomField, updateEvent } = useEventAction();
|
||||
const { updateCustomField } = useEventAction();
|
||||
const featureData = useCuesheet();
|
||||
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
||||
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
|
||||
|
||||
useWindowTitle('Cuesheet');
|
||||
|
||||
/** Handles showing the view params edit drawer */
|
||||
const showEditFormDrawer = useCallback(() => {
|
||||
searchParams.set('edit', 'true');
|
||||
setSearchParams(searchParams);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
/**
|
||||
* Handles updating a custom field
|
||||
*/
|
||||
const handleUpdateCustom = useCallback(
|
||||
async (rowIndex: number, accessor: CustomFieldLabel, payload: string) => {
|
||||
if (!flatRundown || rundownStatus !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowIndex == null || accessor == null || payload == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = flatRundown[rowIndex];
|
||||
if (!event || !isOntimeEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip if there is no value change
|
||||
const previousValue = event.custom[accessor];
|
||||
if (previousValue === payload) {
|
||||
return;
|
||||
}
|
||||
updateCustomField(event.id, accessor, payload);
|
||||
},
|
||||
[flatRundown, rundownStatus, updateCustomField],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles updating all other string fields
|
||||
* Handles updating a field
|
||||
* Currently, only custom fields can be updated from the cuesheet
|
||||
*/
|
||||
const handleUpdate = useCallback(
|
||||
async (rowIndex: number, accessor: keyof OntimeEvent, payload: string) => {
|
||||
async (rowIndex: number, accessor: CustomFieldLabel, payload: unknown) => {
|
||||
if (!flatRundown || rundownStatus !== 'success') {
|
||||
return;
|
||||
}
|
||||
@@ -91,31 +53,29 @@ export default function CuesheetPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip if there is no value change
|
||||
const previousValue = event[accessor];
|
||||
const previousValue = event.custom[accessor];
|
||||
|
||||
if (previousValue === payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateEvent({ id: event.id, [accessor]: payload });
|
||||
},
|
||||
[flatRundown, rundownStatus, updateEvent],
|
||||
);
|
||||
// check if value is valid
|
||||
// in anticipation to different types of event here
|
||||
if (typeof payload !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting the edit modal target and visibility
|
||||
*/
|
||||
const setShowModal = useCallback(
|
||||
(eventId: string | null) => {
|
||||
if (eventId) {
|
||||
setEventId(eventId);
|
||||
onEventEditorOpen();
|
||||
} else {
|
||||
setEventId(null);
|
||||
onEventEditorClose();
|
||||
// cleanup
|
||||
const cleanVal = payload.trim();
|
||||
|
||||
// submit
|
||||
try {
|
||||
await updateCustomField(event.id, accessor, cleanVal);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
[onEventEditorClose, onEventEditorOpen],
|
||||
[flatRundown, rundownStatus, updateCustomField],
|
||||
);
|
||||
|
||||
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
||||
@@ -123,43 +83,32 @@ export default function CuesheetPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isEventEditorOpen} onClose={onEventEditorClose} variant='ontime'>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth='max(640px, 40vw)' padding='1rem'>
|
||||
<CuesheetEventEditor eventId={eventId!} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||
<ViewParamsEditor viewOptions={cuesheetOptions} />
|
||||
<CuesheetOverview>
|
||||
<IconButton
|
||||
aria-label='Toggle navigation'
|
||||
variant='ontime-subtle-white'
|
||||
size='lg'
|
||||
icon={<IoApps />}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label='Toggle settings'
|
||||
variant='ontime-subtle-white'
|
||||
size='lg'
|
||||
icon={<IoSettingsOutline />}
|
||||
onClick={showEditFormDrawer}
|
||||
/>
|
||||
</CuesheetOverview>
|
||||
<CuesheetProgress />
|
||||
<CuesheetDnd columns={columns}>
|
||||
<CuesheetTable
|
||||
data={flatRundown}
|
||||
columns={columns}
|
||||
handleUpdate={handleUpdate}
|
||||
handleUpdateCustom={handleUpdateCustom}
|
||||
showModal={setShowModal}
|
||||
/>
|
||||
</CuesheetDnd>
|
||||
</div>
|
||||
</>
|
||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||
<CuesheetOverview>
|
||||
<IconButton
|
||||
aria-label='Toggle navigation'
|
||||
variant='ontime-subtle-white'
|
||||
size='lg'
|
||||
icon={<IoApps />}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label='Toggle settings'
|
||||
variant='ontime-subtle-white'
|
||||
size='lg'
|
||||
icon={<IoSettingsOutline />}
|
||||
onClick={() => toggleSettings()}
|
||||
/>
|
||||
</CuesheetOverview>
|
||||
<CuesheetProgress />
|
||||
<Cuesheet
|
||||
data={flatRundown}
|
||||
columns={columns}
|
||||
handleUpdate={handleUpdate}
|
||||
selectedId={featureData.selectedEventId}
|
||||
currentBlockId={featureData.currentBlockId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import {
|
||||
closestCorners,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import useColumnManager from '../cuesheet-table/useColumnManager';
|
||||
|
||||
interface CuesheetDndProps {
|
||||
columns: ColumnDef<OntimeRundownEntry>[];
|
||||
}
|
||||
|
||||
export default function CuesheetDnd(props: PropsWithChildren<CuesheetDndProps>) {
|
||||
const { columns, children } = props;
|
||||
|
||||
const { columnOrder, saveColumnOrder } = useColumnManager(columns);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||
const { delta, active, over } = event;
|
||||
|
||||
// cancel if delta y is greater than 200
|
||||
if (delta.y > 200) return;
|
||||
// cancel if we do not have an over id
|
||||
if (over?.id == null) return;
|
||||
|
||||
// get index of from
|
||||
const fromIndex = columnOrder.indexOf(active.id as string);
|
||||
|
||||
// get index of to
|
||||
const toIndex = columnOrder.indexOf(over.id as string);
|
||||
|
||||
if (toIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedCols = [...columnOrder];
|
||||
const reorderedItem = reorderedCols.splice(fromIndex, 1);
|
||||
reorderedCols.splice(toIndex, 0, reorderedItem[0]);
|
||||
saveColumnOrder(reorderedCols);
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCorners} onDragEnd={handleOnDragEnd}>
|
||||
{children}
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
.progressOverride {
|
||||
height: 1rem;
|
||||
grid-area: progress;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import style from '../Cuesheet.module.scss';
|
||||
|
||||
interface BlockRowProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
function BlockRow(props: BlockRowProps) {
|
||||
const { title } = props;
|
||||
return (
|
||||
<tr className={style.blockRow}>
|
||||
<td>{title}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BlockRow);
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
closestCorners,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
|
||||
import { SortableCell } from './SortableCell';
|
||||
|
||||
import style from '../Cuesheet.module.scss';
|
||||
|
||||
interface CuesheetHeaderProps {
|
||||
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||
saveColumnOrder: (fromId: string, toId: string) => void;
|
||||
showIndexColumn: boolean;
|
||||
}
|
||||
|
||||
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||
const { headerGroups, saveColumnOrder, showIndexColumn } = props;
|
||||
|
||||
const handleOnDragEnd = (event: DragEndEvent) => {
|
||||
const { delta, active, over } = event;
|
||||
|
||||
// cancel if delta y is greater than 200
|
||||
if (delta.y > 200) return;
|
||||
// cancel if we do not have an over id
|
||||
if (over?.id == null) return;
|
||||
|
||||
saveColumnOrder(active.id as string, over.id as string);
|
||||
};
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 100,
|
||||
tolerance: 50,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<thead className={style.tableHeader}>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const key = headerGroup.id;
|
||||
|
||||
return (
|
||||
<DndContext key={key} sensors={sensors} collisionDetection={closestCorners} onDragEnd={handleOnDragEnd}>
|
||||
<tr key={headerGroup.id}>
|
||||
<th className={style.indexColumn}>{showIndexColumn && '#'}</th>
|
||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const width = header.getSize();
|
||||
// @ts-expect-error -- we inject this into react-table
|
||||
const customBackground = header.column.columnDef?.meta?.colour;
|
||||
|
||||
let customStyles = {};
|
||||
if (customBackground) {
|
||||
const customColour = getAccessibleColour(customBackground);
|
||||
customStyles = { backgroundColor: customColour.backgroundColor, color: customColour.color };
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableCell key={header.column.columnDef.id} header={header} style={{ width, ...customStyles }}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</SortableCell>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
</DndContext>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||
import { millisToDelayString } from '../../../common/utils/dateConfig';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
import style from '../Cuesheet.module.scss';
|
||||
|
||||
interface DelayRowProps {
|
||||
duration: number;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ChangeEvent, memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getHotkeyHandler } from '@mantine/hooks';
|
||||
|
||||
import { AutoTextArea } from '../../../common/components/input/auto-text-area/AutoTextArea';
|
||||
|
||||
interface EditableCellProps {
|
||||
value: string;
|
||||
handleUpdate: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const EditableCell = (props: EditableCellProps) => {
|
||||
const { value: initialValue, handleUpdate } = props;
|
||||
|
||||
// We need to keep and update the state of the cell normally
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const ref = useRef<HTMLAreaElement>();
|
||||
const onChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => setValue(event.target.value), []);
|
||||
|
||||
// We'll only update the external data when the input is blurred
|
||||
const onBlur = useCallback(() => handleUpdate(value), [handleUpdate, value]);
|
||||
|
||||
//TODO: maybe we can unify this with `useReactiveTextInput`
|
||||
const onKeyDown = getHotkeyHandler([
|
||||
['mod + Enter', () => ref.current?.blur()],
|
||||
[
|
||||
'Escape',
|
||||
() => {
|
||||
setValue(initialValue);
|
||||
setTimeout(() => ref.current?.blur());
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
// If the initialValue is changed external, sync it up with our state
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<AutoTextArea
|
||||
size='sm'
|
||||
value={value}
|
||||
inputref={ref}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
rows={1}
|
||||
onKeyDown={onKeyDown}
|
||||
transition='none'
|
||||
spellCheck={false}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EditableCell);
|
||||
+10
-9
@@ -1,9 +1,10 @@
|
||||
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
||||
import Color from 'color';
|
||||
|
||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
import style from '../Cuesheet.module.scss';
|
||||
|
||||
const pastOpacity = '0.2';
|
||||
|
||||
interface EventRowProps {
|
||||
eventIndex: number;
|
||||
@@ -19,6 +20,9 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const textColour = getAccessibleColour(colour);
|
||||
const bgColour = textColour.backgroundColor;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
@@ -46,16 +50,13 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||
};
|
||||
}, [ownRef, selectedRef]);
|
||||
|
||||
const { color, backgroundColor } = getAccessibleColour(colour);
|
||||
const mutedText = Color(color).fade(0.4).hexa();
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={cx([style.eventRow, skip ?? style.skip])}
|
||||
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
||||
className={`${style.eventRow} ${skip ? style.skip : ''}`}
|
||||
style={{ opacity: `${isPast ? pastOpacity : '1'}` }}
|
||||
ref={selectedRef ?? ownRef}
|
||||
>
|
||||
<td className={style.indexColumn} style={{ backgroundColor, color: mutedText }}>
|
||||
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour.color }}>
|
||||
{showIndexColumn && eventIndex}
|
||||
</td>
|
||||
{isVisible ? children : null}
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { Header } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import styles from '../CuesheetTable.module.scss';
|
||||
import styles from '../Cuesheet.module.scss';
|
||||
|
||||
interface SortableCellProps {
|
||||
header: Header<OntimeRundownEntry, unknown>;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
.tableSettings {
|
||||
grid-area: settings;
|
||||
padding-inline: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
gap: 5rem;
|
||||
font-size: $inner-section-text-size;
|
||||
@@ -26,4 +26,4 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Button, Checkbox } from '@chakra-ui/react';
|
||||
import { Column } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../../features/editors/editor-utils/EditorUtils';
|
||||
import * as Editor from '../../../features/editors/editor-utils/EditorUtils';
|
||||
|
||||
import style from './CuesheetTableSettings.module.scss';
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { IconButton, Menu, MenuButton } from '@chakra-ui/react';
|
||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import {
|
||||
CustomFieldLabel,
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
OntimeRundown,
|
||||
OntimeRundownEntry,
|
||||
} from 'ontime-types';
|
||||
|
||||
import useFollowComponent from '../../../common/hooks/useFollowComponent';
|
||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useCuesheetOptions } from '../cuesheet.options';
|
||||
|
||||
import BlockRow from './cuesheet-table-elements/BlockRow';
|
||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||
import EventRow from './cuesheet-table-elements/EventRow';
|
||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||
import CuesheetTableMenu from './CuesheetTableMenu';
|
||||
import useColumnManager from './useColumnManager';
|
||||
|
||||
import style from './CuesheetTable.module.scss';
|
||||
|
||||
interface CuesheetTableProps {
|
||||
data: OntimeRundown;
|
||||
columns: ColumnDef<OntimeRundownEntry>[];
|
||||
handleUpdate: (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: string) => void;
|
||||
handleUpdateCustom: (rowIndex: number, accessor: CustomFieldLabel, payload: string) => void;
|
||||
showModal: (eventId: MaybeString) => void;
|
||||
}
|
||||
|
||||
export default function CuesheetTable(props: CuesheetTableProps) {
|
||||
const { data, columns, handleUpdate, handleUpdateCustom, showModal } = props;
|
||||
|
||||
const { selectedEventId } = useSelectedEventId();
|
||||
const { followSelected, hideDelays, hidePast, hideIndexColumn } = useCuesheetOptions();
|
||||
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
||||
useColumnManager(columns);
|
||||
|
||||
const selectedRef = useRef<HTMLTableRowElement | null>(null);
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
columnResizeMode: 'onChange',
|
||||
state: {
|
||||
columnOrder,
|
||||
columnVisibility,
|
||||
columnSizing,
|
||||
},
|
||||
meta: {
|
||||
handleUpdate,
|
||||
handleUpdateCustom,
|
||||
},
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const setAllVisible = () => {
|
||||
table.toggleAllColumnsVisible(true);
|
||||
};
|
||||
|
||||
const resetColumnResizing = () => {
|
||||
setColumnSizing({});
|
||||
};
|
||||
|
||||
const headerGroups = table.getHeaderGroups();
|
||||
const rowModel = table.getRowModel();
|
||||
const allLeafColumns = table.getAllLeafColumns();
|
||||
|
||||
let eventIndex = 0;
|
||||
// for the first event, it will be past if there is something selected
|
||||
let isPast = Boolean(selectedEventId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CuesheetTableSettings
|
||||
columns={allLeafColumns}
|
||||
handleResetResizing={resetColumnResizing}
|
||||
handleResetReordering={resetColumnOrder}
|
||||
handleClearToggles={setAllVisible}
|
||||
/>
|
||||
<div ref={tableContainerRef} className={style.cuesheetContainer}>
|
||||
<table className={style.cuesheet} id='cuesheet'>
|
||||
<CuesheetHeader headerGroups={headerGroups} showIndexColumn={!hideIndexColumn} />
|
||||
<tbody>
|
||||
{rowModel.rows.map((row, index) => {
|
||||
const key = row.original.id;
|
||||
const isSelected = selectedEventId === key;
|
||||
const entry = row.original;
|
||||
if (isSelected) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
return <BlockRow key={key} title={entry.title} hidePast={isPast && hidePast} />;
|
||||
}
|
||||
if (isOntimeDelay(entry)) {
|
||||
if (isPast && hidePast) {
|
||||
return null;
|
||||
}
|
||||
const delayVal = entry.duration;
|
||||
if (hideDelays || delayVal === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DelayRow key={key} duration={delayVal} />;
|
||||
}
|
||||
if (isOntimeEvent(entry)) {
|
||||
eventIndex++;
|
||||
const isSelected = key === selectedEventId;
|
||||
|
||||
if (isPast && hidePast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (isSelected) {
|
||||
rowBgColour = '#D20300'; // $red-700
|
||||
} else if (entry.colour) {
|
||||
try {
|
||||
// the colour is user defined and might be invalid
|
||||
const accessibleBackgroundColor = Color(getAccessibleColour(entry.colour).backgroundColor);
|
||||
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
||||
} catch (_error) {
|
||||
/* we do not handle errors here */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu key={key} variant='ontime-on-dark' size='sm' isLazy>
|
||||
<EventRow
|
||||
eventIndex={eventIndex}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
skip={entry.skip}
|
||||
colour={entry.colour}
|
||||
showIndexColumn={!hideIndexColumn}
|
||||
>
|
||||
<td>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size='xs'
|
||||
aria-label='Options'
|
||||
icon={<IoEllipsisHorizontal />}
|
||||
variant='ontime-ghosted'
|
||||
/>
|
||||
</td>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td key={cell.id} style={{ width: cell.column.getSize(), backgroundColor: rowBgColour }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</EventRow>
|
||||
<CuesheetTableMenu event={entry} entryIndex={index} showModal={showModal} />
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
// currently there is no scenario where entryType is not handled above, either way...
|
||||
return null;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
|
||||
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
||||
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { cloneEvent } from '../../../common/utils/eventsManager';
|
||||
|
||||
interface CuesheetTableMenuProps {
|
||||
event: OntimeEvent;
|
||||
entryIndex: number;
|
||||
showModal: (entryId: string) => void;
|
||||
}
|
||||
|
||||
export default function CuesheetTableMenu(props: CuesheetTableMenuProps) {
|
||||
const { event, entryIndex, showModal } = props;
|
||||
const { addEvent, reorderEvent, deleteEvent } = useEventAction();
|
||||
|
||||
const handleCloneEvent = () => {
|
||||
const newEvent = cloneEvent(event);
|
||||
try {
|
||||
addEvent(newEvent, { after: event.id });
|
||||
} catch (_error) {
|
||||
// we do not handle errors here
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
<MenuItem icon={<IoOptions />} onClick={() => showModal(event.id)}>
|
||||
Edit ...
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { before: event.id })}>
|
||||
Add event above
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoAdd />} onClick={() => addEvent({ type: SupportedEvent.Event }, { after: event.id })}>
|
||||
Add event below
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoDuplicateOutline />} onClick={handleCloneEvent}>
|
||||
Clone event
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
isDisabled={entryIndex < 1}
|
||||
icon={<IoArrowUp />}
|
||||
onClick={() => reorderEvent(event.id, entryIndex, entryIndex - 1)}
|
||||
>
|
||||
Move up
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoArrowDown />} onClick={() => reorderEvent(event.id, entryIndex, entryIndex + 1)}>
|
||||
Move down
|
||||
</MenuItem>
|
||||
<MenuItem icon={<IoTrash />} onClick={() => deleteEvent([event.id])}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface BlockRowProps {
|
||||
hidePast: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function BlockRow(props: BlockRowProps) {
|
||||
const { hidePast, title } = props;
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
|
||||
if (hidePast && !currentBlockId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={style.blockRow}>
|
||||
<td>{title}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BlockRow);
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
|
||||
import { SortableCell } from './SortableCell';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface CuesheetHeaderProps {
|
||||
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||
showIndexColumn: boolean;
|
||||
}
|
||||
|
||||
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||
const { headerGroups, showIndexColumn } = props;
|
||||
|
||||
return (
|
||||
<thead className={style.tableHeader}>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const key = headerGroup.id;
|
||||
|
||||
return (
|
||||
<tr key={headerGroup.id}>
|
||||
<th className={style.indexColumn}>{showIndexColumn && '#'}</th>
|
||||
<th className={style.actionColumn} />
|
||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const width = header.getSize();
|
||||
// @ts-expect-error -- we inject this into react-table
|
||||
const customBackground = header.column.columnDef?.meta?.colour;
|
||||
|
||||
let customStyles = {};
|
||||
if (customBackground) {
|
||||
const customColour = getAccessibleColour(customBackground);
|
||||
customStyles = { backgroundColor: customColour.backgroundColor, color: customColour.color };
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableCell key={header.column.columnDef.id} header={header} style={{ width, ...customStyles }}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</SortableCell>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
|
||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
interface MultiLineCellProps {
|
||||
initialValue: string;
|
||||
handleUpdate: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const MultiLineCell = (props: MultiLineCellProps) => {
|
||||
const { initialValue, handleUpdate } = props;
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
submitOnCtrlEnter: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<AutoTextArea
|
||||
inputref={ref}
|
||||
rows={1}
|
||||
size='sm'
|
||||
padding={0}
|
||||
fontSize='1rem'
|
||||
transition='none'
|
||||
variant='ontime-transparent'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
spellCheck={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MultiLineCell);
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
interface SingleLineCellProps {
|
||||
initialValue: string;
|
||||
handleUpdate: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const SingleLineCell = (props: SingleLineCellProps) => {
|
||||
const { initialValue, handleUpdate } = props;
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
submitOnCtrlEnter: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
size='sx'
|
||||
variant='ontime-transparent'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
spellCheck={false}
|
||||
autoComplete='off'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SingleLineCell);
|
||||
@@ -1,90 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||
import { OntimeEntryCommonKeys, OntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* In the specific case of the cuesheet options
|
||||
* we save the user preferences in the local storage
|
||||
* @description set default column order
|
||||
*/
|
||||
export const cuesheetOptions: ViewOption[] = [
|
||||
{ section: 'Table options' },
|
||||
{
|
||||
id: 'hideTableSeconds',
|
||||
title: 'Hide seconds in table',
|
||||
description: 'Whether to hide seconds in the time fields displayed in the table',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'followSelected',
|
||||
title: 'Follow selected event',
|
||||
description: 'Whether the view should automatically scroll to the selected event',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hidePast',
|
||||
title: 'Hide Past Events',
|
||||
description: 'Whether to hide events that have passed',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideIndexColumn',
|
||||
title: 'Hide index column',
|
||||
description: 'Whether the hide the event indexes in the table',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{ section: 'Delay flow' },
|
||||
{
|
||||
id: 'showDelayedTimes',
|
||||
title: 'Show delayed times',
|
||||
description: 'Whether the time fields should include delays',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
id: 'hideDelays',
|
||||
title: 'Hide delays',
|
||||
description: 'Whether to hide the rows containing scheduled delays',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
||||
'isPublic',
|
||||
'cue',
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'duration',
|
||||
'title',
|
||||
'note',
|
||||
];
|
||||
|
||||
type CuesheetOptions = {
|
||||
hideTableSeconds: boolean;
|
||||
followSelected: boolean;
|
||||
hidePast: boolean;
|
||||
hideIndexColumn: boolean;
|
||||
showDelayedTimes: boolean;
|
||||
hideDelays: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility extract the view options from URL Params
|
||||
* the names and fallbacks are manually matched with cuesheetOptions
|
||||
* @description set default hidden columns
|
||||
*/
|
||||
export function getOptionsFromParams(searchParams: URLSearchParams): CuesheetOptions {
|
||||
// we manually make an object that matches the key above
|
||||
return {
|
||||
hideTableSeconds: isStringBoolean(searchParams.get('hideTableSeconds')),
|
||||
followSelected: isStringBoolean(searchParams.get('followSelected')),
|
||||
hidePast: isStringBoolean(searchParams.get('hidePast')),
|
||||
hideIndexColumn: isStringBoolean(searchParams.get('hideIndexColumn')),
|
||||
showDelayedTimes: isStringBoolean(searchParams.get('showDelayedTimes')),
|
||||
hideDelays: isStringBoolean(searchParams.get('hideDelays')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook exposes the cuesheet view options
|
||||
*/
|
||||
export function useCuesheetOptions(): CuesheetOptions {
|
||||
const [searchParams] = useSearchParams();
|
||||
const options = useMemo(() => getOptionsFromParams(searchParams), [searchParams]);
|
||||
return options;
|
||||
}
|
||||
export const defaultHiddenColumns: (keyof OntimeEvent)[] = [];
|
||||
|
||||
+30
-56
@@ -1,84 +1,50 @@
|
||||
import { useCallback } from 'react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||
import RunningTime from '../../../../features/viewers/common/running-time/RunningTime';
|
||||
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||
import DelayIndicator from '../../common/components/delay-indicator/DelayIndicator';
|
||||
import RunningTime from '../../features/viewers/common/running-time/RunningTime';
|
||||
|
||||
import MultiLineCell from './MultiLineCell';
|
||||
import SingleLineCell from './SingleLineCell';
|
||||
import EditableCell from './cuesheet-table-elements/EditableCell';
|
||||
import { useCuesheetSettings } from './store/cuesheetSettingsStore';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
import style from './Cuesheet.module.scss';
|
||||
|
||||
function makePublic(row: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const cellValue = row.getValue();
|
||||
return cellValue ? <IoCheckmark className={style.check} /> : '';
|
||||
}
|
||||
|
||||
function MakeTimer({ getValue, row: { original } }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const { showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
||||
const showDelayedTimes = useCuesheetSettings((state) => state.showDelayedTimes);
|
||||
const hideSeconds = useCuesheetSettings((state) => state.hideSeconds);
|
||||
const cellValue = (getValue() as number | null) ?? 0;
|
||||
const delayValue = (original as OntimeEvent)?.delay ?? 0;
|
||||
|
||||
return (
|
||||
<span className={style.time}>
|
||||
<DelayIndicator delayValue={delayValue} />
|
||||
<RunningTime value={cellValue} hideSeconds={hideTableSeconds} />
|
||||
<RunningTime value={cellValue} hideSeconds={hideSeconds} />
|
||||
{delayValue !== 0 && showDelayedTimes && (
|
||||
<RunningTime className={style.delayedTime} value={cellValue + delayValue} hideSeconds={hideTableSeconds} />
|
||||
<RunningTime className={style.delayedTime} value={cellValue + delayValue} hideSeconds={hideSeconds} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MakeDuration({ getValue }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const { hideTableSeconds } = useCuesheetOptions();
|
||||
const hideSeconds = useCuesheetSettings((state) => state.hideSeconds);
|
||||
const cellValue = (getValue() as number | null) ?? 0;
|
||||
|
||||
return <RunningTime value={cellValue} hideSeconds={hideTableSeconds} />;
|
||||
}
|
||||
|
||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
// @ts-expect-error -- we inject this into react-table
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||
[column.id, row.index],
|
||||
);
|
||||
|
||||
const event = row.original;
|
||||
if (!isOntimeEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
// @ts-expect-error -- we inject this into react-table
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||
[column.id, row.index],
|
||||
);
|
||||
|
||||
const event = row.original;
|
||||
if (!isOntimeEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||
|
||||
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
return <RunningTime value={cellValue} hideSeconds={hideSeconds} />;
|
||||
}
|
||||
|
||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
// @ts-expect-error -- we inject this into react-table
|
||||
table.options.meta?.handleUpdateCustom(row.index, column.id, newValue);
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||
[column.id, row.index],
|
||||
@@ -89,9 +55,10 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
||||
return null;
|
||||
}
|
||||
|
||||
// events dont necessarily contain all custom fields
|
||||
const initialValue = event.custom[column.id] ?? '';
|
||||
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
return <EditableCell value={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
||||
@@ -109,9 +76,16 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
accessorKey: 'cue',
|
||||
id: 'cue',
|
||||
header: 'Cue',
|
||||
cell: MakeSingleLineField,
|
||||
cell: (row) => row.getValue(),
|
||||
size: 75,
|
||||
},
|
||||
{
|
||||
accessorKey: 'isPublic',
|
||||
id: 'isPublic',
|
||||
header: 'Public',
|
||||
cell: makePublic,
|
||||
size: 45,
|
||||
},
|
||||
{
|
||||
accessorKey: 'timeStart',
|
||||
id: 'timeStart',
|
||||
@@ -137,14 +111,14 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
accessorKey: 'title',
|
||||
id: 'title',
|
||||
header: 'Title',
|
||||
cell: MakeSingleLineField,
|
||||
cell: (row) => row.getValue(),
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
accessorKey: 'note',
|
||||
id: 'note',
|
||||
header: 'Note',
|
||||
cell: MakeMultiLineField,
|
||||
cell: (row) => row.getValue(),
|
||||
size: 250,
|
||||
},
|
||||
...dynamicCustomFields,
|
||||
@@ -0,0 +1,85 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { booleanFromLocalStorage } from '../../../common/utils/localStorage';
|
||||
|
||||
interface CuesheetSettingsStore {
|
||||
showSettings: boolean;
|
||||
showIndexColumn: boolean;
|
||||
followSelected: boolean;
|
||||
showPrevious: boolean;
|
||||
showDelayBlock: boolean;
|
||||
showDelayedTimes: boolean;
|
||||
hideSeconds: boolean;
|
||||
|
||||
toggleSettings: (newValue?: boolean) => void;
|
||||
toggleFollow: (newValue?: boolean) => void;
|
||||
togglePreviousVisibility: (newValue?: boolean) => void;
|
||||
toggleIndexColumn: (newValue?: boolean) => void;
|
||||
toggleDelayVisibility: (newValue?: boolean) => void;
|
||||
toggleDelayedTimes: (newValue?: boolean) => void;
|
||||
toggleSecondsVisibility: (newValue?: boolean) => void;
|
||||
}
|
||||
|
||||
function toggle(oldValue: boolean, value?: boolean) {
|
||||
if (typeof value === 'undefined') {
|
||||
return !oldValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
enum CuesheetKeys {
|
||||
Follow = 'ontime-cuesheet-follow-selected',
|
||||
DelayVisibility = 'ontime-cuesheet-show-delay',
|
||||
PreviousVisibility = 'ontime-cuesheet-show-previous',
|
||||
ColumnIndex = 'ontime-cuesheet-show-index-column',
|
||||
DelayedTimes = 'ontime-cuesheet-show-delayed',
|
||||
Seconds = 'ontime-cuesheet-hide-sceconds',
|
||||
}
|
||||
|
||||
export const useCuesheetSettings = create<CuesheetSettingsStore>()((set) => ({
|
||||
showSettings: false,
|
||||
showIndexColumn: booleanFromLocalStorage(CuesheetKeys.ColumnIndex, true),
|
||||
followSelected: booleanFromLocalStorage(CuesheetKeys.Follow, false),
|
||||
showPrevious: booleanFromLocalStorage(CuesheetKeys.PreviousVisibility, true),
|
||||
showDelayBlock: booleanFromLocalStorage(CuesheetKeys.DelayVisibility, true),
|
||||
showDelayedTimes: booleanFromLocalStorage(CuesheetKeys.DelayedTimes, false),
|
||||
hideSeconds: booleanFromLocalStorage(CuesheetKeys.Seconds, false),
|
||||
|
||||
toggleSettings: (newValue?: boolean) => set((state) => ({ showSettings: toggle(state.showSettings, newValue) })),
|
||||
toggleFollow: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const followSelected = toggle(state.followSelected, newValue);
|
||||
localStorage.setItem(CuesheetKeys.Follow, String(followSelected));
|
||||
return { followSelected };
|
||||
}),
|
||||
toggleIndexColumn: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const showIndexColumn = toggle(state.showIndexColumn, newValue);
|
||||
localStorage.setItem(CuesheetKeys.ColumnIndex, String(showIndexColumn));
|
||||
return { showIndexColumn };
|
||||
}),
|
||||
togglePreviousVisibility: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const showPrevious = toggle(state.showPrevious, newValue);
|
||||
localStorage.setItem(CuesheetKeys.PreviousVisibility, String(showPrevious));
|
||||
return { showPrevious };
|
||||
}),
|
||||
toggleDelayVisibility: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const showDelayBlock = toggle(state.showDelayBlock, newValue);
|
||||
localStorage.setItem(CuesheetKeys.DelayVisibility, String(showDelayBlock));
|
||||
return { showDelayBlock };
|
||||
}),
|
||||
toggleDelayedTimes: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const showDelayedTimes = toggle(state.showDelayedTimes, newValue);
|
||||
localStorage.setItem(CuesheetKeys.DelayedTimes, String(showDelayedTimes));
|
||||
return { showDelayedTimes };
|
||||
}),
|
||||
toggleSecondsVisibility: (newValue?: boolean) =>
|
||||
set((state) => {
|
||||
const hideSeconds = toggle(state.hideSeconds, newValue);
|
||||
localStorage.setItem(CuesheetKeys.Seconds, String(hideSeconds));
|
||||
return { hideSeconds };
|
||||
}),
|
||||
}));
|
||||
@@ -23,7 +23,7 @@ if (!isProduction) {
|
||||
}
|
||||
|
||||
/** Flag holds server loading state */
|
||||
let loaded = 'Ontime starting';
|
||||
let loaded = 'Ontime running';
|
||||
|
||||
/**
|
||||
* Flag whether user has requested a quit
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const rundownPostValidator = [
|
||||
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
||||
body('after').optional().isString(),
|
||||
body('before').optional().isString(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
+13
-56
@@ -3,7 +3,7 @@ import { LogOrigin, Playback, SimpleDirection, SimplePlayback } from 'ontime-typ
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import expressStaticGzip from 'express-static-gzip';
|
||||
import http, { Server } from 'http';
|
||||
import http, { type Server } from 'http';
|
||||
import cors from 'cors';
|
||||
import serverTiming from 'server-timing';
|
||||
import { extname } from 'node:path';
|
||||
@@ -179,15 +179,9 @@ export const startServer = async (
|
||||
escalateErrorFn?: (error: string) => void,
|
||||
): Promise<{ message: string; serverPort: number }> => {
|
||||
checkStart(OntimeStartOrder.InitServer);
|
||||
const settings = getDataProvider().getSettings();
|
||||
const { serverPort: desiredPort } = settings;
|
||||
const { serverPort } = getDataProvider().getSettings();
|
||||
|
||||
expressServer = http.createServer(app);
|
||||
|
||||
// the express server must be started before the socket otherwise the on error eventlissner will not attach properly
|
||||
const resultPort = await serverTryDesiredPort(expressServer, desiredPort);
|
||||
await getDataProvider().setSettings({ ...settings, serverPort: resultPort });
|
||||
|
||||
socket.init(expressServer, prefix);
|
||||
|
||||
/**
|
||||
@@ -231,20 +225,19 @@ export const startServer = async (
|
||||
// TODO: pass event store to rundownservice
|
||||
runtimeService.init(maybeRestorePoint);
|
||||
|
||||
const nif = getNetworkInterfaces();
|
||||
consoleSuccess(`Local: http://localhost:${resultPort}${prefix}/editor`);
|
||||
for (const key in nif) {
|
||||
const address = nif[key].address;
|
||||
consoleSuccess(`Network: http://${address}:${resultPort}${prefix}/editor`);
|
||||
}
|
||||
expressServer.listen(serverPort, '0.0.0.0', () => {
|
||||
const nif = getNetworkInterfaces();
|
||||
consoleSuccess(`Local: http://localhost:${serverPort}${prefix}/editor`);
|
||||
for (const key in nif) {
|
||||
const address = nif[key].address;
|
||||
consoleSuccess(`Network: http://${address}:${serverPort}${prefix}/editor`);
|
||||
}
|
||||
});
|
||||
|
||||
const returnMessage = `Ontime is listening on port ${resultPort}`;
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
logger.info(LogOrigin.Server, returnMessage);
|
||||
|
||||
return {
|
||||
message: returnMessage,
|
||||
serverPort: resultPort,
|
||||
};
|
||||
return { message: returnMessage, serverPort };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -314,7 +307,7 @@ process.on('unhandledRejection', async (error) => {
|
||||
consoleError(error.stack);
|
||||
}
|
||||
generateCrashReport(error);
|
||||
logger.crash(LogOrigin.Server, `Uncaught rejection | ${error}`);
|
||||
logger.crash(LogOrigin.Server, `Uncaught exception | ${error}`);
|
||||
await shutdown(1);
|
||||
});
|
||||
|
||||
@@ -331,39 +324,3 @@ process.on('uncaughtException', async (error) => {
|
||||
process.once('SIGHUP', async () => shutdown(0));
|
||||
process.once('SIGINT', async () => shutdown(0));
|
||||
process.once('SIGTERM', async () => shutdown(0));
|
||||
|
||||
/**
|
||||
* @description tries to open the server with the desired port, and if getting a `EADDRINUSE` will change to an efemeral port
|
||||
* @param {http.Server}server http server object
|
||||
* @param {number}desiredPort the desired port
|
||||
* @returns {number} the resulting port number
|
||||
* @throws any other server errors will result in a throw
|
||||
*/
|
||||
async function serverTryDesiredPort(server: http.Server, desiredPort: number): Promise<number> {
|
||||
return new Promise((res) => {
|
||||
expressServer.once('error', (e) => {
|
||||
if (testForPortInUser(e)) {
|
||||
logger.crash(LogOrigin.Server, `Failed open the desired port: ${desiredPort} | to moving to Ephemeral port`);
|
||||
server.listen(0, '0.0.0.0', () => {
|
||||
// @ts-expect-error TODO: find proper documentation for this api
|
||||
const port: number = server.address().port;
|
||||
res(port);
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
server.listen(desiredPort, '0.0.0.0', () => {
|
||||
// @ts-expect-error TODO: find proper documentation for this api
|
||||
const port: number = server.address().port;
|
||||
res(port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function testForPortInUser(err: unknown) {
|
||||
if (typeof err === 'object' && 'code' in err && err.code === 'EADDRINUSE') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
OntimeRundown,
|
||||
PatchWithId,
|
||||
EventPostPayload,
|
||||
} from 'ontime-types';
|
||||
import { getCueCandidate } from 'ontime-utils';
|
||||
|
||||
@@ -24,6 +22,8 @@ import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||
import * as cache from './rundownCache.js';
|
||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||
|
||||
type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
|
||||
|
||||
type CompleteEntry<T> =
|
||||
T extends Partial<OntimeEvent>
|
||||
? OntimeEvent
|
||||
@@ -35,13 +35,12 @@ type CompleteEntry<T> =
|
||||
|
||||
function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>>(
|
||||
eventData: T,
|
||||
afterId?: string,
|
||||
): CompleteEntry<T> {
|
||||
// we discard any UI provided IDs and add our own
|
||||
const id = cache.getUniqueId();
|
||||
|
||||
if (isOntimeEvent(eventData)) {
|
||||
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), afterId)) as CompleteEntry<T>;
|
||||
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as CompleteEntry<T>;
|
||||
}
|
||||
|
||||
if (isOntimeDelay(eventData)) {
|
||||
@@ -60,11 +59,9 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
||||
* @param {object} eventData
|
||||
* @return {OntimeRundownEntry}
|
||||
*/
|
||||
export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundownEntry> {
|
||||
export async function addEvent(eventData: PatchWithId & { after?: string }): Promise<OntimeRundownEntry> {
|
||||
// if the user didnt provide an index, we add the event to start
|
||||
let atIndex = 0;
|
||||
let afterId: string | undefined = eventData?.after;
|
||||
|
||||
if (eventData?.after !== undefined) {
|
||||
const previousIndex = cache.getIndexOf(eventData.after);
|
||||
if (previousIndex < 0) {
|
||||
@@ -72,20 +69,10 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundo
|
||||
} else {
|
||||
atIndex = previousIndex + 1;
|
||||
}
|
||||
} else if (eventData?.before !== undefined) {
|
||||
const previousIndex = cache.getIndexOf(eventData.before);
|
||||
if (previousIndex < 0) {
|
||||
logger.warning(LogOrigin.Server, `Could not find event with id ${eventData.before}`);
|
||||
} else {
|
||||
atIndex = previousIndex;
|
||||
if (previousIndex > 0) {
|
||||
afterId = cache.getPersistedRundown()[atIndex - 1].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate a fully formed event from the patch
|
||||
const eventToAdd = generateEvent(eventData, afterId);
|
||||
const eventToAdd = generateEvent(eventData);
|
||||
|
||||
// modify rundown
|
||||
const scopedMutation = cache.mutateCache(cache.add);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
||||
import { dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||
import { MaybeNumber, Playback, TimerPhase } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
import { RuntimeState } from '../stores/runtimeState.js';
|
||||
|
||||
/**
|
||||
@@ -187,12 +187,25 @@ export function getExpectedEnd(state: RuntimeState): MaybeNumber {
|
||||
return state.runtime.plannedEnd - state.runtime.offset + state._timer.totalDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility checks whether the playback is considered to be active
|
||||
* @param state
|
||||
* @returns
|
||||
*/
|
||||
export function isPlaybackActive(state: RuntimeState): boolean {
|
||||
return (
|
||||
state.timer.playback === Playback.Play ||
|
||||
state.timer.playback === Playback.Pause ||
|
||||
state.timer.playback === Playback.Roll
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks running timer to see which phase it currently is in
|
||||
* @param state
|
||||
*/
|
||||
export function getTimerPhase(state: RuntimeState): TimerPhase {
|
||||
if (!isPlaybackActive(state.timer.playback)) {
|
||||
if (!isPlaybackActive(state)) {
|
||||
return TimerPhase.None;
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,6 @@ describe('mutation on runtimeState', () => {
|
||||
expect(newState.runtime.plannedStart).toBe(0);
|
||||
expect(newState.runtime.plannedEnd).toBe(1500);
|
||||
expect(newState.currentBlock.block).toBeNull();
|
||||
expect(newState.runtime.offset).toBe(0);
|
||||
|
||||
// 2. Start event
|
||||
start();
|
||||
|
||||
@@ -11,14 +11,7 @@ import {
|
||||
TimerPhase,
|
||||
TimerState,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
calculateDuration,
|
||||
checkIsNow,
|
||||
dayInMs,
|
||||
filterTimedEvents,
|
||||
getPreviousBlock,
|
||||
isPlaybackActive,
|
||||
} from 'ontime-utils';
|
||||
import { calculateDuration, checkIsNow, dayInMs, filterTimedEvents, getPreviousBlock } from 'ontime-utils';
|
||||
|
||||
import { clock } from '../services/Clock.js';
|
||||
import { RestorePoint } from '../services/RestoreService.js';
|
||||
@@ -28,6 +21,7 @@ import {
|
||||
getExpectedFinish,
|
||||
getRuntimeOffset,
|
||||
getTimerPhase,
|
||||
isPlaybackActive,
|
||||
} from '../services/timerUtils.js';
|
||||
import { timerConfig } from '../config/config.js';
|
||||
import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
||||
@@ -491,7 +485,7 @@ export function update(): UpdateResult {
|
||||
runtimeState.clock = clock.timeNow(); // we update the clock on every update call
|
||||
|
||||
// 1. is playback idle?
|
||||
if (!isPlaybackActive(runtimeState.timer.playback)) {
|
||||
if (!isPlaybackActive(runtimeState)) {
|
||||
return updateIfIdle();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
test('cuesheet displays events', async ({ page }) => {
|
||||
test('cuesheet displays events and exports csv', async ({ page }) => {
|
||||
// same elements in cuesheet
|
||||
await page.goto('http://localhost:4001/cuesheet');
|
||||
await expect(page.getByText('Eurovision Song Contest')).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'Lunch break' })).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'Afternoon break' })).toBeVisible();
|
||||
|
||||
await expect(page.locator('#cuesheet')).toBeVisible();
|
||||
|
||||
// there should be 16 rows in the table (same as the amount of events in the rundown)
|
||||
const rowCount = await page.locator('#cuesheet tbody tr').count();
|
||||
expect(rowCount).toBe(16);
|
||||
await page.getByText('Eurovision Song Contest').click();
|
||||
await page.getByRole('cell', { name: 'Lunch break' }).click();
|
||||
await page.getByRole('cell', { name: 'Albania' }).click();
|
||||
await page.getByRole('cell', { name: 'Latvia' }).click();
|
||||
await page.getByRole('cell', { name: 'Lithuania' }).click();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { OntimeBlock, OntimeDelay, OntimeEvent } from '../../definitions/core/OntimeEvent.type.js';
|
||||
import type { OntimeRundownEntry } from '../../definitions/core/Rundown.type.js';
|
||||
|
||||
type EventId = string;
|
||||
@@ -9,14 +8,3 @@ export interface RundownCached {
|
||||
order: EventId[];
|
||||
revision: number;
|
||||
}
|
||||
|
||||
export type PatchWithId = Partial<OntimeEvent | OntimeDelay | OntimeBlock> & { id: string };
|
||||
export type EventPostPayload = Partial<OntimeRundownEntry> & {
|
||||
after?: string;
|
||||
before?: string;
|
||||
};
|
||||
|
||||
export type TransientEventPayload = Partial<OntimeEvent | OntimeDelay | OntimeBlock> & {
|
||||
after?: string;
|
||||
before?: string;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum SupportedEvent {
|
||||
export type OntimeBaseEvent = {
|
||||
type: SupportedEvent;
|
||||
id: string;
|
||||
after?: string; // used when creating an event to indicate its position in rundown
|
||||
};
|
||||
|
||||
export type OntimeDelay = OntimeBaseEvent & {
|
||||
|
||||
@@ -55,13 +55,7 @@ export type {
|
||||
ProjectLogoResponse,
|
||||
} from './api/ontime-controller/BackendResponse.type.js';
|
||||
export type { QuickStartData } from './api/db/db.type.js';
|
||||
export type {
|
||||
EventPostPayload,
|
||||
NormalisedRundown,
|
||||
PatchWithId,
|
||||
RundownCached,
|
||||
TransientEventPayload,
|
||||
} from './api/rundown-controller/BackendResponse.type.js';
|
||||
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
||||
|
||||
// SERVER RUNTIME
|
||||
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
||||
|
||||
@@ -89,5 +89,3 @@ export {
|
||||
defaultImportMap,
|
||||
isImportMap,
|
||||
} from './src/feature/spreadsheet-import/spreadsheetImport.js';
|
||||
|
||||
export { isPlaybackActive } from './src/playback-utils/playbackstate.js';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Utility checks whether the playback is considered to be active
|
||||
* @param state
|
||||
* @returns
|
||||
*/
|
||||
export function isPlaybackActive(state: Playback): boolean {
|
||||
return state === Playback.Play || state === Playback.Pause || state === Playback.Roll;
|
||||
}
|
||||
Generated
+674
-154
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user