mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-24 00:19:21 +00:00
refactor: improve cuesheet composition
refactor: simplify placing events in rundown m
This commit is contained in:
committed by
Carlos Valente
parent
c7faab00a3
commit
ec823e0095
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached, TransientEventPayload } from 'ontime-types';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
|||||||
/**
|
/**
|
||||||
* HTTP request to post new event
|
* HTTP request to post new event
|
||||||
*/
|
*/
|
||||||
export async function requestPostEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
export async function requestPostEvent(data: TransientEventPayload): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
return axios.post(rundownPath, data);
|
return axios.post(rundownPath, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import {
|
||||||
|
isOntimeEvent,
|
||||||
|
OntimeBlock,
|
||||||
|
OntimeDelay,
|
||||||
|
OntimeEvent,
|
||||||
|
OntimeRundownEntry,
|
||||||
|
RundownCached,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from 'ontime-types';
|
||||||
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
import { dayInMs, MILLIS_PER_SECOND, parseUserTime, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN } from '../api/constants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
@@ -19,6 +27,16 @@ import {
|
|||||||
import { logAxiosError } from '../api/utils';
|
import { logAxiosError } from '../api/utils';
|
||||||
import { useEditorSettings } from '../stores/editorSettings';
|
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
|
* @description Set of utilities for events //TODO: should this be called useEntryAction and so on
|
||||||
*/
|
*/
|
||||||
@@ -47,31 +65,19 @@ export const useEventAction = () => {
|
|||||||
networkMode: 'always',
|
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
|
* Adds an event to rundown
|
||||||
*/
|
*/
|
||||||
const addEvent = useCallback(
|
const addEvent = useCallback(
|
||||||
async (event: Partial<OntimeRundownEntry>, options?: EventOptions) => {
|
async (event: Partial<OntimeEvent | OntimeDelay | OntimeBlock>, options?: EventOptions) => {
|
||||||
const newEvent: Partial<OntimeRundownEntry> = { ...event };
|
const newEvent: TransientEventPayload = { ...event };
|
||||||
|
|
||||||
// ************* CHECK OPTIONS specific to events
|
// ************* CHECK OPTIONS specific to events
|
||||||
if (isOntimeEvent(newEvent)) {
|
if (isOntimeEvent(newEvent)) {
|
||||||
// merge creation time options with event settings
|
// merge creation time options with event settings
|
||||||
const applicationOptions = {
|
const applicationOptions = {
|
||||||
after: options?.after,
|
after: options?.after,
|
||||||
|
before: options?.before,
|
||||||
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
defaultPublic: options?.defaultPublic ?? defaultPublic,
|
||||||
lastEventId: options?.lastEventId,
|
lastEventId: options?.lastEventId,
|
||||||
linkPrevious: options?.linkPrevious ?? linkPrevious,
|
linkPrevious: options?.linkPrevious ?? linkPrevious,
|
||||||
@@ -121,11 +127,16 @@ export const useEventAction = () => {
|
|||||||
|
|
||||||
// handle adding options that concern all event type
|
// handle adding options that concern all event type
|
||||||
if (options?.after) {
|
if (options?.after) {
|
||||||
|
// @ts-expect-error -- not sure how to type this, <after> is a transient property
|
||||||
newEvent.after = options.after;
|
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 {
|
try {
|
||||||
await _addEventMutation.mutateAsync(newEvent);
|
await _addEventMutation.mutateAsync(newEvent as TransientEventPayload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Failed adding event', error);
|
logAxiosError('Failed adding event', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,19 +149,21 @@ export const setAuxTimer = {
|
|||||||
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCuesheet = () => {
|
export const useSelectedEventId = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
playback: state.timer.playback,
|
|
||||||
currentBlockId: state.currentBlock.block?.id ?? null,
|
|
||||||
selectedEventId: state.eventNow?.id ?? null,
|
selectedEventId: state.eventNow?.id ?? null,
|
||||||
selectedEventIndex: state.runtime.selectedEventIndex,
|
|
||||||
numEvents: state.runtime.numEvents,
|
|
||||||
titleNow: state.eventNow?.title || '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCurrentBlockId = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
currentBlockId: state.currentBlock.block?.id ?? null,
|
||||||
|
});
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
export const setEventPlayback = {
|
export const setEventPlayback = {
|
||||||
loadEvent: (id: string) => socketSendJson('load', { id }),
|
loadEvent: (id: string) => socketSendJson('load', { id }),
|
||||||
startEvent: (id: string) => socketSendJson('start', { id }),
|
startEvent: (id: string) => socketSendJson('start', { id }),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
|||||||
* @return {OntimeEvent} clean event
|
* @return {OntimeEvent} clean event
|
||||||
*/
|
*/
|
||||||
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
||||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
export const cloneEvent = (event: OntimeEvent): ClonedEvent => {
|
||||||
return {
|
return {
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
@@ -23,7 +23,6 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
|||||||
isPublic: event.isPublic,
|
isPublic: event.isPublic,
|
||||||
skip: event.skip,
|
skip: event.skip,
|
||||||
colour: event.colour,
|
colour: event.colour,
|
||||||
after,
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: event.timeWarning,
|
timeWarning: event.timeWarning,
|
||||||
timeDanger: event.timeDanger,
|
timeDanger: event.timeDanger,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
|
MaybeString,
|
||||||
PlayableEvent,
|
PlayableEvent,
|
||||||
Playback,
|
Playback,
|
||||||
RundownCached,
|
RundownCached,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
isNewLatest,
|
isNewLatest,
|
||||||
} from 'ontime-utils';
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
@@ -82,36 +83,36 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const cloneEntry = rundown[copyId];
|
const cloneEntry = rundown[copyId];
|
||||||
if (cloneEntry?.type === SupportedEvent.Event) {
|
if (cloneEntry?.type === SupportedEvent.Event) {
|
||||||
//if we don't have a cursor add the new event on top
|
//if we don't have a cursor add the new event on top
|
||||||
const newEvent = cloneEvent(cloneEntry, adjustedCursor ?? undefined);
|
const newEvent = cloneEvent(cloneEntry);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent, { after: adjustedCursor ?? undefined });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addEvent, order, rundown],
|
[addEvent, order, rundown],
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertAtId = useCallback(
|
const insertAtId = useCallback(
|
||||||
(type: SupportedEvent, id: string | null, above = false) => {
|
(type: SupportedEvent, id: MaybeString, above = false) => {
|
||||||
const adjustedCursor = above ? getPreviousNormal(rundown, order, id ?? '').entry?.id ?? null : id;
|
const options: EventOptions =
|
||||||
if (adjustedCursor === null) {
|
id === null
|
||||||
// the only thing to do is adding an event at top
|
? {}
|
||||||
addEvent({ type });
|
: {
|
||||||
return;
|
after: above ? undefined : id,
|
||||||
}
|
before: above ? id : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
if (type === SupportedEvent.Event) {
|
if (type === SupportedEvent.Event) {
|
||||||
const newEvent = {
|
const newEvent = {
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
};
|
};
|
||||||
const options = {
|
if (!above && id) {
|
||||||
after: adjustedCursor,
|
options.lastEventId = id;
|
||||||
lastEventId: adjustedCursor,
|
}
|
||||||
};
|
|
||||||
addEvent(newEvent, options);
|
addEvent(newEvent, options);
|
||||||
} else {
|
} else {
|
||||||
addEvent({ type }, { after: adjustedCursor });
|
addEvent({ type }, options);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rundown, order, addEvent],
|
[addEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectBlock = useCallback(
|
const selectBlock = useCallback(
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
return deleteEvent([data.id]);
|
return deleteEvent([data.id]);
|
||||||
}
|
}
|
||||||
case 'clone': {
|
case 'clone': {
|
||||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
const newEvent = cloneEvent(data as OntimeEvent);
|
||||||
addEvent(newEvent, { after: data.id });
|
addEvent(newEvent, { after: data.id });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,6 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
border-top: 1px solid $white-10;
|
|
||||||
padding-top: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeSettings {
|
.timeSettings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { CSSProperties, memo, useCallback, useEffect, useState } from 'react';
|
import { CSSProperties, useCallback } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
import { CustomFieldLabel, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import CopyTag from '../../../common/components/copy-tag/CopyTag';
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||||
import useRundown from '../../../common/hooks-query/useRundown';
|
|
||||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
||||||
import { useEventSelection } from '../useEventSelection';
|
|
||||||
|
|
||||||
import EventEditorTimes from './composite/EventEditorTimes';
|
import EventEditorTimes from './composite/EventEditorTimes';
|
||||||
import EventEditorTitles from './composite/EventEditorTitles';
|
import EventEditorTitles from './composite/EventEditorTitles';
|
||||||
@@ -22,35 +19,17 @@ export type EventEditorSubmitActions = keyof OntimeEvent;
|
|||||||
|
|
||||||
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel;
|
||||||
|
|
||||||
export default function EventEditor() {
|
interface EventEditorProps {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
event: OntimeEvent;
|
||||||
const { data } = useRundown();
|
}
|
||||||
|
|
||||||
|
export default function EventEditor(props: EventEditorProps) {
|
||||||
|
const { event } = props;
|
||||||
const { data: customFields } = useCustomFields();
|
const { data: customFields } = useCustomFields();
|
||||||
const { order, rundown } = data;
|
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
const [_searchParams, setSearchParams] = useSearchParams();
|
const [_searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
const isEditor = window.location.pathname.includes('editor');
|
||||||
|
|
||||||
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(
|
const handleSubmit = useCallback(
|
||||||
(field: EditorUpdateFields, value: string) => {
|
(field: EditorUpdateFields, value: string) => {
|
||||||
@@ -73,87 +52,61 @@ export default function EventEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
<div className={style.content}>
|
||||||
<div className={style.content}>
|
<EventEditorTimes
|
||||||
<EventEditorTimes
|
key={`${event.id}-times`}
|
||||||
key={`${event.id}-times`}
|
eventId={event.id}
|
||||||
eventId={event.id}
|
timeStart={event.timeStart}
|
||||||
timeStart={event.timeStart}
|
timeEnd={event.timeEnd}
|
||||||
timeEnd={event.timeEnd}
|
duration={event.duration}
|
||||||
duration={event.duration}
|
timeStrategy={event.timeStrategy}
|
||||||
timeStrategy={event.timeStrategy}
|
linkStart={event.linkStart}
|
||||||
linkStart={event.linkStart}
|
isTimeToEnd={event.isTimeToEnd}
|
||||||
isTimeToEnd={event.isTimeToEnd}
|
delay={event.delay ?? 0}
|
||||||
delay={event.delay ?? 0}
|
isPublic={event.isPublic}
|
||||||
isPublic={event.isPublic}
|
endAction={event.endAction}
|
||||||
endAction={event.endAction}
|
timerType={event.timerType}
|
||||||
timerType={event.timerType}
|
timeWarning={event.timeWarning}
|
||||||
timeWarning={event.timeWarning}
|
timeDanger={event.timeDanger}
|
||||||
timeDanger={event.timeDanger}
|
/>
|
||||||
/>
|
<EventEditorTitles
|
||||||
<EventEditorTitles
|
key={`${event.id}-titles`}
|
||||||
key={`${event.id}-titles`}
|
eventId={event.id}
|
||||||
eventId={event.id}
|
cue={event.cue}
|
||||||
cue={event.cue}
|
title={event.title}
|
||||||
title={event.title}
|
note={event.note}
|
||||||
note={event.note}
|
colour={event.colour}
|
||||||
colour={event.colour}
|
handleSubmit={handleSubmit}
|
||||||
handleSubmit={handleSubmit}
|
/>
|
||||||
/>
|
<div className={style.column}>
|
||||||
<div className={style.column}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<Editor.Title>Custom Fields</Editor.Title>
|
||||||
<Editor.Title>Custom Fields</Editor.Title>
|
{isEditor && (
|
||||||
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
<Button variant='ontime-subtle' size='sm' onClick={handleOpenCustomManager}>
|
||||||
Manage
|
Manage
|
||||||
</Button>
|
</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;
|
|
||||||
|
|
||||||
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>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
$table-font-size: calc(1rem - 2px);
|
|
||||||
$table-header-font-size: calc(1rem - 3px);
|
|
||||||
|
|
||||||
.cuesheetContainer {
|
|
||||||
grid-area: table;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
padding-bottom: 640px; // allow focus to reach last elements
|
|
||||||
}
|
|
||||||
|
|
||||||
.cuesheet {
|
|
||||||
font-size: $table-font-size;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
tr {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
margin: 1px;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
text-align: left;
|
|
||||||
position: relative;
|
|
||||||
@include ellipsis-overflow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableHeader,
|
|
||||||
.eventRow {
|
|
||||||
.indexColumn {
|
|
||||||
min-width: 2rem;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: 400;
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: $gray-1300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableHeader {
|
|
||||||
position: sticky;
|
|
||||||
top: 0px;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: $ui-black;
|
|
||||||
font-size: $table-header-font-size;
|
|
||||||
color: $label-gray;}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: $gray-1300;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.resizer {
|
|
||||||
width: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.eventRow {
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
outline: 1px solid $blue-700;
|
|
||||||
outline-offset: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
background-color: $gray-1250;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.skip {
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: $opacity-disabled !important; // fighting inline styles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockRow {
|
|
||||||
width: 100%;
|
|
||||||
background-color: $gray-1350;
|
|
||||||
font-size: 1rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
|
|
||||||
td {
|
|
||||||
align-self: flex-end;
|
|
||||||
position: sticky;
|
|
||||||
left: 1rem;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.delayRow {
|
|
||||||
width: 100%;
|
|
||||||
color: $ontime-delay-text;
|
|
||||||
|
|
||||||
td {
|
|
||||||
position: sticky;
|
|
||||||
left: 47.5%; // center of the screen, ish
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
&:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
@include ellipsis-overflow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.delayedTime {
|
|
||||||
color: $ontime-delay-text;
|
|
||||||
font-size: calc(1rem - 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resizer {
|
|
||||||
cursor: col-resize;
|
|
||||||
opacity: $opacity-disabled;
|
|
||||||
display: inline-block;
|
|
||||||
width: 0;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
background-color: $action-blue;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
touch-action: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
|
||||||
import Color from 'color';
|
|
||||||
import {
|
|
||||||
CustomFieldLabel,
|
|
||||||
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 { useCuesheetOptions } from './cuesheet.options';
|
|
||||||
import useColumnManager from './useColumnManager';
|
|
||||||
|
|
||||||
import style from './Cuesheet.module.scss';
|
|
||||||
|
|
||||||
interface CuesheetProps {
|
|
||||||
data: OntimeRundown;
|
|
||||||
columns: ColumnDef<OntimeRundownEntry>[];
|
|
||||||
handleUpdate: (rowIndex: number, accessor: keyof OntimeRundownEntry, payload: string) => void;
|
|
||||||
handleUpdateCustom: (rowIndex: number, accessor: CustomFieldLabel, payload: string) => void;
|
|
||||||
selectedId: string | null;
|
|
||||||
currentBlockId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Cuesheet({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
handleUpdate,
|
|
||||||
handleUpdateCustom,
|
|
||||||
selectedId,
|
|
||||||
currentBlockId,
|
|
||||||
}: CuesheetProps) {
|
|
||||||
const { followSelected, hideDelays, hidePast, hideIndexColumn } = useCuesheetOptions();
|
|
||||||
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,
|
|
||||||
handleUpdateCustom,
|
|
||||||
},
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<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={!hideIndexColumn} />
|
|
||||||
<tbody>
|
|
||||||
{rowModel.rows.map((row) => {
|
|
||||||
const key = row.original.id;
|
|
||||||
const isSelected = selectedId === key;
|
|
||||||
if (isSelected) {
|
|
||||||
isPast = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOntimeBlock(row.original)) {
|
|
||||||
if (isPast && hidePast && key !== currentBlockId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <BlockRow key={key} title={row.original.title} />;
|
|
||||||
}
|
|
||||||
if (isOntimeDelay(row.original)) {
|
|
||||||
if (isPast && hidePast) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const delayVal = row.original.duration;
|
|
||||||
if (hideDelays || delayVal === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DelayRow key={key} duration={delayVal} />;
|
|
||||||
}
|
|
||||||
if (isOntimeEvent(row.original)) {
|
|
||||||
eventIndex++;
|
|
||||||
const isSelected = key === selectedId;
|
|
||||||
|
|
||||||
if (isPast && hidePast) {
|
|
||||||
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={!hideIndexColumn}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { IconButton, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, Modal, ModalContent, ModalOverlay, useDisclosure } from '@chakra-ui/react';
|
||||||
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
import { IoApps } from '@react-icons/all-files/io5/IoApps';
|
||||||
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
|
||||||
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
import { CustomFieldLabel, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||||
@@ -9,16 +9,17 @@ import ProductionNavigationMenu from '../../common/components/navigation-menu/Pr
|
|||||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useCuesheet } from '../../common/hooks/useSocket';
|
|
||||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||||
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../common/hooks-query/useCustomFields';
|
||||||
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
import { useFlatRundown } from '../../common/hooks-query/useRundown';
|
||||||
import { CuesheetOverview } from '../../features/overview/Overview';
|
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 CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import Cuesheet from './Cuesheet';
|
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||||
import { cuesheetOptions } from './cuesheet.options';
|
import { cuesheetOptions } from './cuesheet.options';
|
||||||
import { makeCuesheetColumns } from './cuesheetCols';
|
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols';
|
||||||
|
|
||||||
import styles from './CuesheetPage.module.scss';
|
import styles from './CuesheetPage.module.scss';
|
||||||
|
|
||||||
@@ -28,9 +29,10 @@ export default function CuesheetPage() {
|
|||||||
const { data: customFields } = useCustomFields();
|
const { data: customFields } = useCustomFields();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { isOpen: isMenuOpen, onOpen, onClose } = useDisclosure();
|
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, updateEvent } = useEventAction();
|
||||||
const featureData = useCuesheet();
|
|
||||||
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]);
|
||||||
|
|
||||||
useWindowTitle('Cuesheet');
|
useWindowTitle('Cuesheet');
|
||||||
@@ -100,40 +102,64 @@ export default function CuesheetPage() {
|
|||||||
[flatRundown, rundownStatus, updateEvent],
|
[flatRundown, rundownStatus, updateEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles setting the edit modal target and visibility
|
||||||
|
*/
|
||||||
|
const setShowModal = useCallback(
|
||||||
|
(eventId: string | null) => {
|
||||||
|
if (eventId) {
|
||||||
|
setEventId(eventId);
|
||||||
|
onEventEditorOpen();
|
||||||
|
} else {
|
||||||
|
setEventId(null);
|
||||||
|
onEventEditorClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onEventEditorClose, onEventEditorOpen],
|
||||||
|
);
|
||||||
|
|
||||||
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
if (!customFields || !flatRundown || rundownStatus !== 'success') {
|
||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyPage text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<>
|
||||||
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
<Modal isOpen={isEventEditorOpen} onClose={onEventEditorClose} variant='ontime'>
|
||||||
<ViewParamsEditor viewOptions={cuesheetOptions} />
|
<ModalOverlay />
|
||||||
<CuesheetOverview>
|
<ModalContent maxWidth='max(640px, 40vw)' padding='1rem'>
|
||||||
<IconButton
|
<CuesheetEventEditor eventId={eventId!} />
|
||||||
aria-label='Toggle navigation'
|
</ModalContent>
|
||||||
variant='ontime-subtle-white'
|
</Modal>
|
||||||
size='lg'
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
icon={<IoApps />}
|
<ProductionNavigationMenu isMenuOpen={isMenuOpen} onMenuClose={onClose} />
|
||||||
onClick={onOpen}
|
<ViewParamsEditor viewOptions={cuesheetOptions} />
|
||||||
/>
|
<CuesheetOverview>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle settings'
|
aria-label='Toggle navigation'
|
||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
size='lg'
|
size='lg'
|
||||||
icon={<IoSettingsOutline />}
|
icon={<IoApps />}
|
||||||
onClick={showEditFormDrawer}
|
onClick={onOpen}
|
||||||
/>
|
/>
|
||||||
</CuesheetOverview>
|
<IconButton
|
||||||
<CuesheetProgress />
|
aria-label='Toggle settings'
|
||||||
<Cuesheet
|
variant='ontime-subtle-white'
|
||||||
data={flatRundown}
|
size='lg'
|
||||||
columns={columns}
|
icon={<IoSettingsOutline />}
|
||||||
handleUpdate={handleUpdate}
|
onClick={showEditFormDrawer}
|
||||||
handleUpdateCustom={handleUpdateCustom}
|
/>
|
||||||
//TODO: stabilizer selectedEventId and currentBlockId
|
</CuesheetOverview>
|
||||||
selectedId={featureData.selectedEventId}
|
<CuesheetProgress />
|
||||||
currentBlockId={featureData.currentBlockId}
|
<CuesheetDnd columns={columns}>
|
||||||
/>
|
<CuesheetTable
|
||||||
</div>
|
data={flatRundown}
|
||||||
|
columns={columns}
|
||||||
|
handleUpdate={handleUpdate}
|
||||||
|
handleUpdateCustom={handleUpdateCustom}
|
||||||
|
showModal={setShowModal}
|
||||||
|
/>
|
||||||
|
</CuesheetDnd>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import { millisToDelayString } from '../../../common/utils/dateConfig';
|
|
||||||
|
|
||||||
import style from '../Cuesheet.module.scss';
|
|
||||||
|
|
||||||
interface DelayRowProps {
|
|
||||||
duration: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DelayRow(props: DelayRowProps) {
|
|
||||||
const { duration } = props;
|
|
||||||
const delayTime = millisToDelayString(duration, 'expanded');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={style.delayRow}>
|
|
||||||
<td>{delayTime}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(DelayRow);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
|
||||||
|
|
||||||
import style from '../Cuesheet.module.scss';
|
|
||||||
|
|
||||||
const pastOpacity = '0.2';
|
|
||||||
|
|
||||||
interface EventRowProps {
|
|
||||||
eventIndex: number;
|
|
||||||
showIndexColumn: boolean;
|
|
||||||
isPast?: boolean;
|
|
||||||
selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
|
|
||||||
skip?: boolean;
|
|
||||||
colour?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventRow(props: PropsWithChildren<EventRowProps>) {
|
|
||||||
const { children, eventIndex, isPast, selectedRef, skip, colour, showIndexColumn } = props;
|
|
||||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
|
|
||||||
const textColour = getAccessibleColour(colour);
|
|
||||||
const bgColour = textColour.backgroundColor;
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setIsVisible(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: null,
|
|
||||||
threshold: 0.01,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRefCurrent = ownRef.current;
|
|
||||||
if (selectedRef) {
|
|
||||||
setIsVisible(true);
|
|
||||||
} else if (handleRefCurrent) {
|
|
||||||
observer.observe(handleRefCurrent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (handleRefCurrent) {
|
|
||||||
observer.unobserve(handleRefCurrent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [ownRef, selectedRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
className={`${style.eventRow} ${skip ? style.skip : ''}`}
|
|
||||||
style={{ opacity: `${isPast ? pastOpacity : '1'}` }}
|
|
||||||
ref={selectedRef ?? ownRef}
|
|
||||||
>
|
|
||||||
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour.color }}>
|
|
||||||
{showIndexColumn && eventIndex}
|
|
||||||
</td>
|
|
||||||
{isVisible ? children : null}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(EventRow);
|
|
||||||
@@ -1,37 +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'
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
transition='none'
|
|
||||||
variant='ontime-transparent'
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(MultiLineCell);
|
|
||||||
@@ -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,44 +0,0 @@
|
|||||||
import { CSSProperties, ReactNode } from 'react';
|
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { Header } from '@tanstack/react-table';
|
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
|
||||||
|
|
||||||
import styles from '../Cuesheet.module.scss';
|
|
||||||
|
|
||||||
interface SortableCellProps {
|
|
||||||
header: Header<OntimeRundownEntry, unknown>;
|
|
||||||
style: CSSProperties;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SortableCell({ header, style, children }: SortableCellProps) {
|
|
||||||
const { column, colSpan } = header;
|
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
||||||
id: column.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// build drag styles
|
|
||||||
const dragStyle = {
|
|
||||||
...style,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
transform: CSS.Translate.toString(transform),
|
|
||||||
transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan}>
|
|
||||||
<div {...attributes} {...listeners}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
{...{
|
|
||||||
onMouseDown: header.getResizeHandler(),
|
|
||||||
onTouchStart: header.getResizeHandler(),
|
|
||||||
}}
|
|
||||||
className={styles.resizer}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
.tableSettings {
|
|
||||||
grid-area: settings;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 5rem;
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
|
|
||||||
@media (max-width: $small-screen) {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
column-gap: 1rem;
|
|
||||||
row-gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { memo, ReactNode } from 'react';
|
|
||||||
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 style from './CuesheetTableSettings.module.scss';
|
|
||||||
|
|
||||||
// reusable button styles
|
|
||||||
const buttonProps = {
|
|
||||||
size: 'xs',
|
|
||||||
variant: 'ontime-subtle',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CuesheetTableSettingsProps {
|
|
||||||
columns: Column<OntimeRundownEntry, unknown>[];
|
|
||||||
handleResetResizing: () => void;
|
|
||||||
handleResetReordering: () => void;
|
|
||||||
handleClearToggles: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
|
||||||
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.tableSettings}>
|
|
||||||
<div>
|
|
||||||
<Editor.Label className={style.sectionTitle}>Toggle column visibility</Editor.Label>
|
|
||||||
<div className={style.row}>
|
|
||||||
{columns.map((column) => {
|
|
||||||
const columnHeader = column.columnDef.header;
|
|
||||||
const visible = column.getIsVisible();
|
|
||||||
return (
|
|
||||||
<label key={`${column.id}-${visible}`} className={style.option}>
|
|
||||||
<Checkbox
|
|
||||||
variant='ontime-ondark'
|
|
||||||
defaultChecked={visible}
|
|
||||||
onChange={column.getToggleVisibilityHandler()}
|
|
||||||
/>
|
|
||||||
{columnHeader as ReactNode}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.column}>
|
|
||||||
<Editor.Label className={style.sectionTitle}>Reset Options</Editor.Label>
|
|
||||||
<div className={style.row}>
|
|
||||||
<Button onClick={handleClearToggles} {...buttonProps}>
|
|
||||||
Show All
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleResetResizing} {...buttonProps}>
|
|
||||||
Reset Resizing
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleResetReordering} {...buttonProps}>
|
|
||||||
Reset Reordering
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(CuesheetTableSettings);
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { Checkbox } from '@chakra-ui/react';
|
|
||||||
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 MultiLineCell from './cuesheet-table-elements/MultiLineCell';
|
|
||||||
import SingleLineCell from './cuesheet-table-elements/SingleLineCell';
|
|
||||||
import { useCuesheetOptions } from './cuesheet.options';
|
|
||||||
|
|
||||||
import style from './Cuesheet.module.scss';
|
|
||||||
|
|
||||||
function MakePublic({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
|
||||||
const update = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// @ts-expect-error -- we inject this into react-table
|
|
||||||
table.options.meta?.handleUpdate(row.index, column.id, event.target.checked);
|
|
||||||
},
|
|
||||||
// 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 isChecked = event.isPublic;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox variant='ontime-ondark' onChange={update} isChecked={isChecked} style={{ verticalAlign: 'middle' }} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakeTimer({ getValue, row: { original } }: CellContext<OntimeRundownEntry, unknown>) {
|
|
||||||
const { showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
|
||||||
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} />
|
|
||||||
{delayValue !== 0 && showDelayedTimes && (
|
|
||||||
<RunningTime className={style.delayedTime} value={cellValue + delayValue} hideSeconds={hideTableSeconds} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MakeDuration({ getValue }: CellContext<OntimeRundownEntry, unknown>) {
|
|
||||||
const { hideTableSeconds } = useCuesheetOptions();
|
|
||||||
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} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
// 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.custom[column.id] ?? '';
|
|
||||||
|
|
||||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
|
||||||
const dynamicCustomFields = Object.keys(customFields).map((key) => ({
|
|
||||||
accessorKey: key,
|
|
||||||
id: key,
|
|
||||||
header: customFields[key].label,
|
|
||||||
meta: { colour: customFields[key].colour },
|
|
||||||
cell: MakeCustomField,
|
|
||||||
size: 250,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessorKey: 'cue',
|
|
||||||
id: 'cue',
|
|
||||||
header: 'Cue',
|
|
||||||
cell: (row) => row.getValue(),
|
|
||||||
size: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'isPublic',
|
|
||||||
id: 'isPublic',
|
|
||||||
header: 'Public',
|
|
||||||
cell: MakePublic,
|
|
||||||
size: 45,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'timeStart',
|
|
||||||
id: 'timeStart',
|
|
||||||
header: 'Start',
|
|
||||||
cell: MakeTimer,
|
|
||||||
size: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'timeEnd',
|
|
||||||
id: 'timeEnd',
|
|
||||||
header: 'End',
|
|
||||||
cell: MakeTimer,
|
|
||||||
size: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'duration',
|
|
||||||
id: 'duration',
|
|
||||||
header: 'Duration',
|
|
||||||
cell: MakeDuration,
|
|
||||||
size: 75,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'title',
|
|
||||||
id: 'title',
|
|
||||||
header: 'Title',
|
|
||||||
cell: MakeSingleLineField,
|
|
||||||
size: 250,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'note',
|
|
||||||
id: 'note',
|
|
||||||
header: 'Note',
|
|
||||||
cell: MakeMultiLineField,
|
|
||||||
size: 250,
|
|
||||||
},
|
|
||||||
...dynamicCustomFields,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
|
||||||
|
|
||||||
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
|
||||||
key: 'table-order',
|
|
||||||
defaultValue: columns.map((col) => col.id as string),
|
|
||||||
});
|
|
||||||
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
|
|
||||||
|
|
||||||
// if the columns change, we update the dataset
|
|
||||||
useEffect(() => {
|
|
||||||
let shouldReplace = false;
|
|
||||||
const newColumns: string[] = [];
|
|
||||||
|
|
||||||
// iterate through columns to see if there are new ids
|
|
||||||
columns.forEach((column) => {
|
|
||||||
const columnnId = column.id as string;
|
|
||||||
if (!shouldReplace && !columnOrder.includes(columnnId)) {
|
|
||||||
shouldReplace = true;
|
|
||||||
}
|
|
||||||
newColumns.push(columnnId);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldReplace) {
|
|
||||||
saveColumnOrder(newColumns);
|
|
||||||
}
|
|
||||||
}, [columnOrder, columns, saveColumnOrder]);
|
|
||||||
|
|
||||||
const resetColumnOrder = useCallback(() => {
|
|
||||||
saveColumnOrder(columns.map((col) => col.id as string));
|
|
||||||
}, [columns, saveColumnOrder]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
columnVisibility,
|
|
||||||
columnOrder,
|
|
||||||
columnSizing,
|
|
||||||
resetColumnOrder,
|
|
||||||
setColumnVisibility,
|
|
||||||
saveColumnOrder,
|
|
||||||
setColumnSizing,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,8 @@ import { Request, Response, NextFunction } from 'express';
|
|||||||
|
|
||||||
export const rundownPostValidator = [
|
export const rundownPostValidator = [
|
||||||
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
body('type').isString().exists().isIn(['event', 'delay', 'block']),
|
||||||
|
body('after').optional().isString(),
|
||||||
|
body('before').optional().isString(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
|
PatchWithId,
|
||||||
|
EventPostPayload,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { getCueCandidate } from 'ontime-utils';
|
import { getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -22,8 +24,6 @@ import { runtimeService } from '../runtime-service/RuntimeService.js';
|
|||||||
import * as cache from './rundownCache.js';
|
import * as cache from './rundownCache.js';
|
||||||
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
import { getPlayableEvents, getTimedEvents } from './rundownUtils.js';
|
||||||
|
|
||||||
type PatchWithId = (Partial<OntimeEvent> | Partial<OntimeBlock> | Partial<OntimeDelay>) & { id: string };
|
|
||||||
|
|
||||||
type CompleteEntry<T> =
|
type CompleteEntry<T> =
|
||||||
T extends Partial<OntimeEvent>
|
T extends Partial<OntimeEvent>
|
||||||
? OntimeEvent
|
? OntimeEvent
|
||||||
@@ -35,12 +35,13 @@ type CompleteEntry<T> =
|
|||||||
|
|
||||||
function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>>(
|
function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | Partial<OntimeBlock>>(
|
||||||
eventData: T,
|
eventData: T,
|
||||||
|
afterId?: string,
|
||||||
): CompleteEntry<T> {
|
): CompleteEntry<T> {
|
||||||
// we discard any UI provided IDs and add our own
|
// we discard any UI provided IDs and add our own
|
||||||
const id = cache.getUniqueId();
|
const id = cache.getUniqueId();
|
||||||
|
|
||||||
if (isOntimeEvent(eventData)) {
|
if (isOntimeEvent(eventData)) {
|
||||||
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as CompleteEntry<T>;
|
return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), afterId)) as CompleteEntry<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOntimeDelay(eventData)) {
|
if (isOntimeDelay(eventData)) {
|
||||||
@@ -59,9 +60,11 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
|||||||
* @param {object} eventData
|
* @param {object} eventData
|
||||||
* @return {OntimeRundownEntry}
|
* @return {OntimeRundownEntry}
|
||||||
*/
|
*/
|
||||||
export async function addEvent(eventData: PatchWithId & { after?: string }): Promise<OntimeRundownEntry> {
|
export async function addEvent(eventData: EventPostPayload): Promise<OntimeRundownEntry> {
|
||||||
// if the user didnt provide an index, we add the event to start
|
// if the user didnt provide an index, we add the event to start
|
||||||
let atIndex = 0;
|
let atIndex = 0;
|
||||||
|
let afterId: string | undefined = eventData?.after;
|
||||||
|
|
||||||
if (eventData?.after !== undefined) {
|
if (eventData?.after !== undefined) {
|
||||||
const previousIndex = cache.getIndexOf(eventData.after);
|
const previousIndex = cache.getIndexOf(eventData.after);
|
||||||
if (previousIndex < 0) {
|
if (previousIndex < 0) {
|
||||||
@@ -69,10 +72,20 @@ export async function addEvent(eventData: PatchWithId & { after?: string }): Pro
|
|||||||
} else {
|
} else {
|
||||||
atIndex = previousIndex + 1;
|
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
|
// generate a fully formed event from the patch
|
||||||
const eventToAdd = generateEvent(eventData);
|
const eventToAdd = generateEvent(eventData, afterId);
|
||||||
|
|
||||||
// modify rundown
|
// modify rundown
|
||||||
const scopedMutation = cache.mutateCache(cache.add);
|
const scopedMutation = cache.mutateCache(cache.add);
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ test('cuesheet displays events', async ({ page }) => {
|
|||||||
await page.goto('http://localhost:4001/cuesheet');
|
await page.goto('http://localhost:4001/cuesheet');
|
||||||
await expect(page.getByText('Eurovision Song Contest')).toBeVisible();
|
await expect(page.getByText('Eurovision Song Contest')).toBeVisible();
|
||||||
await expect(page.getByRole('row', { name: 'Lunch break' })).toBeVisible();
|
await expect(page.getByRole('row', { name: 'Lunch break' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('row', { name: 'Afternoon break' })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.locator('tr:nth-child(1) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue(
|
await expect(page.locator('#cuesheet')).toBeVisible();
|
||||||
'Albania',
|
|
||||||
);
|
// there should be 16 rows in the table (same as the amount of events in the rundown)
|
||||||
await expect(page.locator('tr:nth-child(2) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue(
|
const rowCount = await page.locator('#cuesheet tbody tr').count();
|
||||||
'Latvia',
|
expect(rowCount).toBe(16);
|
||||||
);
|
|
||||||
await expect(page.locator('tr:nth-child(3) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue(
|
|
||||||
'Lithuania',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { OntimeBlock, OntimeDelay, OntimeEvent } from '../../definitions/core/OntimeEvent.type.js';
|
||||||
import type { OntimeRundownEntry } from '../../definitions/core/Rundown.type.js';
|
import type { OntimeRundownEntry } from '../../definitions/core/Rundown.type.js';
|
||||||
|
|
||||||
type EventId = string;
|
type EventId = string;
|
||||||
@@ -8,3 +9,14 @@ export interface RundownCached {
|
|||||||
order: EventId[];
|
order: EventId[];
|
||||||
revision: number;
|
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,7 +9,6 @@ export enum SupportedEvent {
|
|||||||
export type OntimeBaseEvent = {
|
export type OntimeBaseEvent = {
|
||||||
type: SupportedEvent;
|
type: SupportedEvent;
|
||||||
id: string;
|
id: string;
|
||||||
after?: string; // used when creating an event to indicate its position in rundown
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OntimeDelay = OntimeBaseEvent & {
|
export type OntimeDelay = OntimeBaseEvent & {
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ export type {
|
|||||||
ProjectLogoResponse,
|
ProjectLogoResponse,
|
||||||
} from './api/ontime-controller/BackendResponse.type.js';
|
} from './api/ontime-controller/BackendResponse.type.js';
|
||||||
export type { QuickStartData } from './api/db/db.type.js';
|
export type { QuickStartData } from './api/db/db.type.js';
|
||||||
export type { RundownCached, NormalisedRundown } from './api/rundown-controller/BackendResponse.type.js';
|
export type {
|
||||||
|
EventPostPayload,
|
||||||
|
NormalisedRundown,
|
||||||
|
PatchWithId,
|
||||||
|
RundownCached,
|
||||||
|
TransientEventPayload,
|
||||||
|
} from './api/rundown-controller/BackendResponse.type.js';
|
||||||
|
|
||||||
// SERVER RUNTIME
|
// SERVER RUNTIME
|
||||||
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user