diff --git a/apps/client/src/common/api/rundown.ts b/apps/client/src/common/api/rundown.ts index 41fd721cb..9d854eee1 100644 --- a/apps/client/src/common/api/rundown.ts +++ b/apps/client/src/common/api/rundown.ts @@ -1,5 +1,5 @@ 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'; @@ -16,7 +16,7 @@ export async function fetchNormalisedRundown(): Promise { /** * HTTP request to post new event */ -export async function requestPostEvent(data: Partial): Promise> { +export async function requestPostEvent(data: TransientEventPayload): Promise> { return axios.post(rundownPath, data); } diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 54e0af182..d094ff9b4 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -1,6 +1,14 @@ import { useCallback } from 'react'; 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 { RUNDOWN } from '../api/constants'; @@ -19,6 +27,16 @@ 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 */ @@ -47,31 +65,19 @@ 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, options?: EventOptions) => { - const newEvent: Partial = { ...event }; + async (event: Partial, options?: EventOptions) => { + const newEvent: TransientEventPayload = { ...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, @@ -121,11 +127,16 @@ export const useEventAction = () => { // handle adding options that concern all event type if (options?.after) { + // @ts-expect-error -- not sure how to type this, is a transient property newEvent.after = options.after; } + if (options?.before) { + // @ts-expect-error -- not sure how to type this, is a transient property + newEvent.before = options.before; + } try { - await _addEventMutation.mutateAsync(newEvent); + await _addEventMutation.mutateAsync(newEvent as TransientEventPayload); } catch (error) { logAxiosError('Failed adding event', error); } diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index 815b5a392..6bd7c934c 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -149,19 +149,21 @@ export const setAuxTimer = { setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }), }; -export const useCuesheet = () => { +export const useSelectedEventId = () => { 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); }; +export const useCurrentBlockId = () => { + const featureSelector = (state: RuntimeStore) => ({ + currentBlockId: state.currentBlock.block?.id ?? null, + }); + return useRuntimeStore(featureSelector); +}; + export const setEventPlayback = { loadEvent: (id: string) => socketSendJson('load', { id }), startEvent: (id: string) => socketSendJson('start', { id }), diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index eb0370b26..81bf483b6 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -7,7 +7,7 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types'; * @return {OntimeEvent} clean event */ type ClonedEvent = Omit; -export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { +export const cloneEvent = (event: OntimeEvent): ClonedEvent => { return { type: SupportedEvent.Event, title: event.title, @@ -23,7 +23,6 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { isPublic: event.isPublic, skip: event.skip, colour: event.colour, - after, revision: 0, timeWarning: event.timeWarning, timeDanger: event.timeDanger, diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index ce77e8361..3343c7e20 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -6,6 +6,7 @@ import { isOntimeBlock, isOntimeEvent, isPlayableEvent, + MaybeString, PlayableEvent, Playback, RundownCached, @@ -21,7 +22,7 @@ import { isNewLatest, } from 'ontime-utils'; -import { useEventAction } from '../../common/hooks/useEventAction'; +import { type EventOptions, useEventAction } from '../../common/hooks/useEventAction'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useRundownEditor } from '../../common/hooks/useSocket'; import { AppMode, useAppMode } from '../../common/stores/appModeStore'; @@ -82,36 +83,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, adjustedCursor ?? undefined); - addEvent(newEvent); + const newEvent = cloneEvent(cloneEntry); + addEvent(newEvent, { after: adjustedCursor ?? undefined }); } }, [addEvent, order, rundown], ); const insertAtId = useCallback( - (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; - } + (type: SupportedEvent, id: MaybeString, above = false) => { + const options: EventOptions = + id === null + ? {} + : { + after: above ? undefined : id, + before: above ? id : undefined, + }; if (type === SupportedEvent.Event) { const newEvent = { type: SupportedEvent.Event, }; - const options = { - after: adjustedCursor, - lastEventId: adjustedCursor, - }; + if (!above && id) { + options.lastEventId = id; + } addEvent(newEvent, options); } else { - addEvent({ type }, { after: adjustedCursor }); + addEvent({ type }, options); } }, - [rundown, order, addEvent], + [addEvent], ); const selectBlock = useCallback( diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index 1166961f1..806d2f62c 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -115,7 +115,7 @@ export default function RundownEntry(props: RundownEntryProps) { return deleteEvent([data.id]); } case 'clone': { - const newEvent = cloneEvent(data as OntimeEvent, data.id); + const newEvent = cloneEvent(data as OntimeEvent); addEvent(newEvent, { after: data.id }); break; } diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss index 7f9b8469b..6e274a102 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/rundown/event-editor/EventEditor.module.scss @@ -20,14 +20,6 @@ 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; diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.tsx b/apps/client/src/features/rundown/event-editor/EventEditor.tsx index 15f8efa3f..05a111059 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/event-editor/EventEditor.tsx @@ -1,15 +1,12 @@ -import { CSSProperties, memo, useCallback, useEffect, useState } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; 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 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'; @@ -22,35 +19,17 @@ export type EventEditorSubmitActions = keyof OntimeEvent; export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | CustomFieldLabel; -export default function EventEditor() { - const selectedEvents = useEventSelection((state) => state.selectedEvents); - const { data } = useRundown(); +interface EventEditorProps { + event: OntimeEvent; +} + +export default function EventEditor(props: EventEditorProps) { + const { event } = props; const { data: customFields } = useCustomFields(); - const { order, rundown } = data; const { updateEvent } = useEventAction(); const [_searchParams, setSearchParams] = useSearchParams(); - const [event, setEvent] = useState(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 isEditor = window.location.pathname.includes('editor'); const handleSubmit = useCallback( (field: EditorUpdateFields, value: string) => { @@ -73,87 +52,61 @@ export default function EventEditor() { } return ( -
-
- - -
-
- Custom Fields +
+ + +
+
+ Custom Fields + {isEditor && ( -
- {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 ( - - ); - })} + )}
+ {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 ( + + ); + })}
- -
- ); -} - -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 ( -
- - {loadById} - - - {loadByCue} -
); } diff --git a/apps/client/src/views/cuesheet/Cuesheet.module.scss b/apps/client/src/views/cuesheet/Cuesheet.module.scss deleted file mode 100644 index a5064eb87..000000000 --- a/apps/client/src/views/cuesheet/Cuesheet.module.scss +++ /dev/null @@ -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; - } -} diff --git a/apps/client/src/views/cuesheet/Cuesheet.tsx b/apps/client/src/views/cuesheet/Cuesheet.tsx deleted file mode 100644 index d205da34b..000000000 --- a/apps/client/src/views/cuesheet/Cuesheet.tsx +++ /dev/null @@ -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[]; - 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(null); - const tableContainerRef = useRef(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 ( - <> - -
- - - - {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 ; - } - if (isOntimeDelay(row.original)) { - if (isPast && hidePast) { - return null; - } - const delayVal = row.original.duration; - if (hideDelays || delayVal === 0) { - return null; - } - - return ; - } - 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 ( - - {row.getVisibleCells().map((cell) => { - return ( - - ); - })} - - ); - } - - // currently there is no scenario where entryType is not handled above, either way... - return null; - })} - -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- - ); -} diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx index e59184527..2dd92c457 100644 --- a/apps/client/src/views/cuesheet/CuesheetPage.tsx +++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx @@ -1,6 +1,6 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; 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 { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline'; 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 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 Cuesheet from './Cuesheet'; +import CuesheetTable from './cuesheet-table/CuesheetTable'; import { cuesheetOptions } from './cuesheet.options'; -import { makeCuesheetColumns } from './cuesheetCols'; +import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetCols'; import styles from './CuesheetPage.module.scss'; @@ -28,9 +29,10 @@ export default function CuesheetPage() { 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(null); const { updateCustomField, updateEvent } = useEventAction(); - const featureData = useCuesheet(); const columns = useMemo(() => makeCuesheetColumns(customFields), [customFields]); useWindowTitle('Cuesheet'); @@ -100,40 +102,64 @@ export default function CuesheetPage() { [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') { return ; } return ( -
- - - - } - onClick={onOpen} - /> - } - onClick={showEditFormDrawer} - /> - - - -
+ <> + + + + + + +
+ + + + } + onClick={onOpen} + /> + } + onClick={showEditFormDrawer} + /> + + + + + +
+ ); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/BlockRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/BlockRow.tsx deleted file mode 100644 index 7b2b398d0..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/BlockRow.tsx +++ /dev/null @@ -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 ( - - {title} - - ); -} - -export default memo(BlockRow); diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx deleted file mode 100644 index 192d88663..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx +++ /dev/null @@ -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[]; - 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 ( - - {headerGroups.map((headerGroup) => { - const key = headerGroup.id; - - return ( - - - {showIndexColumn && '#'} - - {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 ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - - - ); - })} - - ); -} diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/DelayRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/DelayRow.tsx deleted file mode 100644 index 0149cc991..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/DelayRow.tsx +++ /dev/null @@ -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 ( - - {delayTime} - - ); -} - -export default memo(DelayRow); diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/EventRow.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/EventRow.tsx deleted file mode 100644 index 53fd7f11d..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/EventRow.tsx +++ /dev/null @@ -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; - skip?: boolean; - colour?: string; -} - -function EventRow(props: PropsWithChildren) { - const { children, eventIndex, isPast, selectedRef, skip, colour, showIndexColumn } = props; - const ownRef = useRef(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 ( - - - {showIndexColumn && eventIndex} - - {isVisible ? children : null} - - ); -} - -export default memo(EventRow); diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/MultiLineCell.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/MultiLineCell.tsx deleted file mode 100644 index 9fa159863..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/MultiLineCell.tsx +++ /dev/null @@ -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(null); - const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); - - const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, { - submitOnCtrlEnter: true, - }); - - return ( - - ); -}; - -export default memo(MultiLineCell); diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/SingleLineCell.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/SingleLineCell.tsx deleted file mode 100644 index 329d8a4f9..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/SingleLineCell.tsx +++ /dev/null @@ -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(null); - const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]); - - const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, { - submitOnCtrlEnter: true, - }); - - return ( - - ); -}; - -export default memo(SingleLineCell); diff --git a/apps/client/src/views/cuesheet/cuesheet-table-elements/SortableCell.tsx b/apps/client/src/views/cuesheet/cuesheet-table-elements/SortableCell.tsx deleted file mode 100644 index 07cb0509c..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-elements/SortableCell.tsx +++ /dev/null @@ -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; - 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 ( - -
- {children} -
-
- - ); -} diff --git a/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.module.scss b/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.module.scss deleted file mode 100644 index a036cda7c..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.module.scss +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx b/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx deleted file mode 100644 index fd7c04424..000000000 --- a/apps/client/src/views/cuesheet/cuesheet-table-settings/CuesheetTableSettings.tsx +++ /dev/null @@ -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[]; - handleResetResizing: () => void; - handleResetReordering: () => void; - handleClearToggles: () => void; -} - -function CuesheetTableSettings(props: CuesheetTableSettingsProps) { - const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props; - - return ( -
-
- Toggle column visibility -
- {columns.map((column) => { - const columnHeader = column.columnDef.header; - const visible = column.getIsVisible(); - return ( - - ); - })} -
-
-
- Reset Options -
- - - -
-
-
- ); -} - -export default memo(CuesheetTableSettings); diff --git a/apps/client/src/views/cuesheet/cuesheetCols.tsx b/apps/client/src/views/cuesheet/cuesheetCols.tsx deleted file mode 100644 index f82a55561..000000000 --- a/apps/client/src/views/cuesheet/cuesheetCols.tsx +++ /dev/null @@ -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) { - const update = useCallback( - (event: React.ChangeEvent) => { - // @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 ( - - ); -} - -function MakeTimer({ getValue, row: { original } }: CellContext) { - const { showDelayedTimes, hideTableSeconds } = useCuesheetOptions(); - const cellValue = (getValue() as number | null) ?? 0; - const delayValue = (original as OntimeEvent)?.delay ?? 0; - - return ( - - - - {delayValue !== 0 && showDelayedTimes && ( - - )} - - ); -} - -function MakeDuration({ getValue }: CellContext) { - const { hideTableSeconds } = useCuesheetOptions(); - const cellValue = (getValue() as number | null) ?? 0; - - return ; -} - -function MakeMultiLineField({ row, column, table }: CellContext) { - 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 ; -} - -function MakeSingleLineField({ row, column, table }: CellContext) { - 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 ; -} - -function MakeCustomField({ row, column, table }: CellContext) { - 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 ; -} - -export function makeCuesheetColumns(customFields: CustomFields): ColumnDef[] { - 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, - ]; -} diff --git a/apps/client/src/views/cuesheet/useColumnManager.tsx b/apps/client/src/views/cuesheet/useColumnManager.tsx deleted file mode 100644 index 7e8e33aaa..000000000 --- a/apps/client/src/views/cuesheet/useColumnManager.tsx +++ /dev/null @@ -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[]) { - const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} }); - const [columnOrder, saveColumnOrder] = useLocalStorage({ - 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, - }; -} diff --git a/apps/server/src/api-data/rundown/rundown.validation.ts b/apps/server/src/api-data/rundown/rundown.validation.ts index 2de741ff9..c0b020e8c 100644 --- a/apps/server/src/api-data/rundown/rundown.validation.ts +++ b/apps/server/src/api-data/rundown/rundown.validation.ts @@ -3,6 +3,8 @@ 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); diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index 3fa8c3ef7..473481794 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -9,6 +9,8 @@ import { isOntimeDelay, isOntimeEvent, OntimeRundown, + PatchWithId, + EventPostPayload, } from 'ontime-types'; import { getCueCandidate } from 'ontime-utils'; @@ -22,8 +24,6 @@ import { runtimeService } from '../runtime-service/RuntimeService.js'; import * as cache from './rundownCache.js'; import { getPlayableEvents, getTimedEvents } from './rundownUtils.js'; -type PatchWithId = (Partial | Partial | Partial) & { id: string }; - type CompleteEntry = T extends Partial ? OntimeEvent @@ -35,12 +35,13 @@ type CompleteEntry = function generateEvent | Partial | Partial>( eventData: T, + afterId?: string, ): CompleteEntry { // we discard any UI provided IDs and add our own const id = cache.getUniqueId(); if (isOntimeEvent(eventData)) { - return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), eventData?.after)) as CompleteEntry; + return createEvent(eventData, getCueCandidate(cache.getPersistedRundown(), afterId)) as CompleteEntry; } if (isOntimeDelay(eventData)) { @@ -59,9 +60,11 @@ function generateEvent | Partial | P * @param {object} eventData * @return {OntimeRundownEntry} */ -export async function addEvent(eventData: PatchWithId & { after?: string }): Promise { +export async function addEvent(eventData: EventPostPayload): Promise { // 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) { @@ -69,10 +72,20 @@ export async function addEvent(eventData: PatchWithId & { after?: string }): Pro } 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); + const eventToAdd = generateEvent(eventData, afterId); // modify rundown const scopedMutation = cache.mutateCache(cache.add); diff --git a/e2e/tests/features/202-cuesheet.spec.ts b/e2e/tests/features/202-cuesheet.spec.ts index df98815e7..7289286d4 100644 --- a/e2e/tests/features/202-cuesheet.spec.ts +++ b/e2e/tests/features/202-cuesheet.spec.ts @@ -5,14 +5,11 @@ test('cuesheet displays events', async ({ page }) => { 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('tr:nth-child(1) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue( - 'Albania', - ); - await expect(page.locator('tr:nth-child(2) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue( - 'Latvia', - ); - await expect(page.locator('tr:nth-child(3) > td:nth-child(7)').first().getByRole('textbox').first()).toHaveValue( - 'Lithuania', - ); + 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); }); diff --git a/packages/types/src/api/rundown-controller/BackendResponse.type.ts b/packages/types/src/api/rundown-controller/BackendResponse.type.ts index c6140e611..f2d7538e9 100644 --- a/packages/types/src/api/rundown-controller/BackendResponse.type.ts +++ b/packages/types/src/api/rundown-controller/BackendResponse.type.ts @@ -1,3 +1,4 @@ +import type { OntimeBlock, OntimeDelay, OntimeEvent } from '../../definitions/core/OntimeEvent.type.js'; import type { OntimeRundownEntry } from '../../definitions/core/Rundown.type.js'; type EventId = string; @@ -8,3 +9,14 @@ export interface RundownCached { order: EventId[]; revision: number; } + +export type PatchWithId = Partial & { id: string }; +export type EventPostPayload = Partial & { + after?: string; + before?: string; +}; + +export type TransientEventPayload = Partial & { + after?: string; + before?: string; +}; diff --git a/packages/types/src/definitions/core/OntimeEvent.type.ts b/packages/types/src/definitions/core/OntimeEvent.type.ts index 6cef9896c..801cf295d 100644 --- a/packages/types/src/definitions/core/OntimeEvent.type.ts +++ b/packages/types/src/definitions/core/OntimeEvent.type.ts @@ -9,7 +9,6 @@ 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 & { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 82e37ee90..11d80f8eb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -55,7 +55,13 @@ export type { ProjectLogoResponse, } from './api/ontime-controller/BackendResponse.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 export { type Log, LogLevel, type LogMessage, LogOrigin } from './definitions/runtime/Logger.type.js';