diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index a9984b30e..e70ae8f2c 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -4,22 +4,18 @@ import { millisToString } from 'ontime-utils'; import { useEmitLog } from '../../../stores/logger'; import { forgivingStringToMillis } from '../../../utils/dateConfig'; -import { TimeEntryField } from '../../../utils/timesManager'; import style from './TimeInput.module.scss'; -interface TimeInputProps { - id?: TimeEntryField; - name: TimeEntryField; - submitHandler: (field: TimeEntryField, value: number) => void; +interface TimeInputProps { + name: T; + submitHandler: (field: T, value: string) => void; time?: number; - delay?: number; placeholder: string; - previousEnd?: number; className?: string; } -export default function TimeInput(props: TimeInputProps) { - const { id, name, submitHandler, time = 0, delay = 0, placeholder, previousEnd = 0, className } = props; +export default function TimeInput(props: TimeInputProps) { + const { name, submitHandler, time = 0, placeholder, className } = props; const { emitError } = useEmitLog(); const inputRef = useRef(null); const [value, setValue] = useState(''); @@ -55,32 +51,20 @@ export default function TimeInput(props: TimeInputProps) { return false; } - let newValMillis = 0; - - // check for known aliases - if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') { - // string to pass should be the time of the end before - if (previousEnd != null) { - newValMillis = previousEnd; - } - } else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) { - // string to pass should add to the end before - const val = newValue.substring(1); - newValMillis = previousEnd + forgivingStringToMillis(val); - } else { - // convert entered value to milliseconds - newValMillis = forgivingStringToMillis(newValue); + // we dont know the values in the rundown, escalate to handler + if (newValue.startsWith('p') || newValue.startsWith('+')) { + submitHandler(name, newValue); } - // check if time is different from before - if (newValMillis === time) return false; - - // update entry - submitHandler(name, newValMillis); + const valueInMillis = forgivingStringToMillis(newValue); + if (valueInMillis === time) { + return false; + } + submitHandler(name, newValue); return true; }, - [name, previousEnd, submitHandler, time], + [name, submitHandler, time], ); /** @@ -90,15 +74,11 @@ export default function TimeInput(props: TimeInputProps) { const validateAndSubmit = useCallback( (newValue: string) => { const success = handleSubmit(newValue); - if (success) { - const ms = forgivingStringToMillis(newValue); - const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay); - setValue(millisToString(delayed)); - } else { + if (!success) { resetValue(); } }, - [delay, handleSubmit, name, resetValue], + [handleSubmit, resetValue], ); /** @@ -144,7 +124,6 @@ export default function TimeInput(props: TimeInputProps) { void; +interface TimeInputWithButtonProps { + name: T; + submitHandler: (field: T, value: string) => void; time?: number; - delay?: number; + hasDelay?: boolean; placeholder: string; - previousEnd?: number; - warning?: string; } -function ButtonInitial(name: TimeEntryField) { - if (name === 'timeStart') return 'S'; - if (name === 'timeEnd') return 'E'; - if (name === 'durationOverride') return 'D'; - if (name === 'timeWarning') return 'Wa'; - if (name === 'timeDanger') return 'Da'; - return ''; -} +export default function TimeInputWithButton(props: TimeInputWithButtonProps) { + const { name, submitHandler, time, hasDelay, placeholder } = props; -function ButtonTooltip(name: TimeEntryField, warning?: string) { - if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`; - if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`; - if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`; - if (name === 'timeWarning') return `Warning${warning ? `: ${warning}` : ''}`; - if (name === 'timeDanger') return `Danger${warning ? `: ${warning}` : ''}`; - return ''; -} - -export default function TimeInputWithButton(props: TimeInputProps) { - const { id, name, submitHandler, time = 0, delay = 0, placeholder, previousEnd = 0, warning } = props; - - const isDelayed = delay !== 0; - const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]); - const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]); - - const TooltipLabel = useMemo(() => { - return ButtonTooltip(name, warning); - }, [name, warning]); - - const ButtonText = useMemo(() => { - return ButtonInitial(name); - }, [name]); + const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]); return ( - - - name={name} submitHandler={submitHandler} time={time} - delay={delay} placeholder={placeholder} - previousEnd={previousEnd} className={style.inputField} /> diff --git a/apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.tsx b/apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.tsx index 10af3d969..905d4d220 100644 --- a/apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.tsx +++ b/apps/client/src/common/components/navigation-menu/rename-client-modal/RenameClientModal.tsx @@ -30,7 +30,7 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal const handleRename = async () => { if (newName) { - await setClientName(newName); + setClientName(newName); persistName(newName); onClose(); } diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index c695fe8f2..4873b6067 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { GetRundownCached, isOntimeEvent, OntimeRundownEntry } from 'ontime-types'; -import { getCueCandidate, swapOntimeEvents } from 'ontime-utils'; +import { getPreviousEvent, swapOntimeEvents } from 'ontime-utils'; import { RUNDOWN } from '../api/apiConstants'; import { logAxiosError } from '../api/apiUtils'; @@ -18,6 +18,7 @@ import { SwapEntry, } from '../api/eventsApi'; import { useEditorSettings } from '../stores/editorSettings'; +import { forgivingStringToMillis } from '../utils/dateConfig'; /** * @description Set of utilities for events @@ -70,19 +71,9 @@ export const useEventAction = () => { const rundown = queryClient.getQueryData(RUNDOWN)?.rundown ?? []; - if (newEvent?.cue === undefined) { - newEvent.cue = getCueCandidate(rundown, options?.after); - } - - // hard coding duration value to be as expected for now - // this until timeOptions gets implemented - if (newEvent?.timeStart !== undefined && newEvent.timeEnd !== undefined) { - newEvent.duration = Math.max(0, newEvent?.timeEnd - newEvent?.timeStart) || 0; - } - if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) { const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId); - if (previousEvent !== undefined && previousEvent.type === 'event') { + if (isOntimeEvent(previousEvent)) { newEvent.timeStart = previousEvent.timeEnd; newEvent.timeEnd = previousEvent.timeEnd; } @@ -120,7 +111,6 @@ export const useEventAction = () => { await queryClient.cancelQueries({ queryKey: RUNDOWN }); // Snapshot the previous value - const previousData = queryClient.getQueryData(RUNDOWN); if (previousData) { @@ -164,6 +154,50 @@ export const useEventAction = () => { [_updateEventMutation], ); + type TimeField = 'timeStart' | 'timeEnd' | 'duration'; + /** + * Updates time of existing event + */ + const updateTimer = useCallback( + async (eventId: string, field: TimeField, value: string) => { + const getPreviousEnd = (): number => { + const rundown = queryClient.getQueryData(RUNDOWN)?.rundown ?? []; + if (rundown) { + const { previousEvent } = getPreviousEvent(rundown, eventId); + if (previousEvent) { + return previousEvent.timeEnd; + } + } + return 0; + }; + + let newValMillis = 0; + + // check for previous keyword + if (value === 'p' || value === 'prev' || value === 'previous') { + newValMillis = getPreviousEnd(); + + // check for adding time keyword + } else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) { + const remainingString = value.substring(1); + newValMillis = getPreviousEnd() + forgivingStringToMillis(remainingString); + } else { + newValMillis = forgivingStringToMillis(value); + } + + const newEvent = { + id: eventId, + [field]: newValMillis, + }; + try { + await _updateEventMutation.mutateAsync(newEvent); + } catch (error) { + logAxiosError('Error updating event', error); + } + }, + [_updateEventMutation, queryClient], + ); + /** * Calls mutation to edit multiple events * @private @@ -392,9 +426,9 @@ export const useEventAction = () => { async (eventId: string, from: number, to: number) => { try { const reorderObject: ReorderEntry = { - eventId: eventId, - from: from, - to: to, + eventId, + from, + to, }; await _reorderEventMutation.mutateAsync(reorderObject); } catch (error) { @@ -461,12 +495,13 @@ export const useEventAction = () => { return { addEvent, - updateEvent, + applyDelay, + batchUpdateEvents, deleteEvent, deleteAllEvents, - applyDelay, reorderEvent, swapEvents, - batchUpdateEvents, + updateEvent, + updateTimer, }; }; diff --git a/apps/client/src/common/utils/eventsManager.ts b/apps/client/src/common/utils/eventsManager.ts index cee75bf9e..f41b8988a 100644 --- a/apps/client/src/common/utils/eventsManager.ts +++ b/apps/client/src/common/utils/eventsManager.ts @@ -8,13 +8,12 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types'; */ type ClonedEvent = Omit< OntimeEvent, - 'id' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9' + 'id' | 'cue' | 'user0' | 'user1' | 'user2' | 'user3' | 'user4' | 'user5' | 'user6' | 'user7' | 'user8' | 'user9' >; export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { return { type: SupportedEvent.Event, title: event.title, - cue: event.cue, subtitle: event.subtitle, presenter: event.presenter, note: event.note, @@ -26,7 +25,7 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => { isPublic: event.isPublic, skip: event.skip, colour: event.colour, - after: after, + after, revision: 0, timeWarning: event.timeWarning, timeDanger: event.timeDanger, diff --git a/apps/client/src/common/utils/timesManager.ts b/apps/client/src/common/utils/timesManager.ts deleted file mode 100644 index 2c9c41297..000000000 --- a/apps/client/src/common/utils/timesManager.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timeWarning' | 'timeDanger'; - -/** - * @description Checks which field the value relates to - */ -export const handleTimeEntry = ( - field: TimeEntryField, - val: number, - timeStart: number, - timeEnd: number, -): { start: number; end: number; durationOverride: boolean } => { - let start = timeStart; - let end = timeEnd; - let durationOverride = false; - - if (field === 'timeStart') { - start = val; - } else if (field === 'timeEnd') { - end = val; - } else { - durationOverride = field === 'durationOverride'; - } - return { start, end, durationOverride }; -}; - -/** - * @description Validates time entry - */ -export const validateEntry = ( - field: TimeEntryField, - value: number, - timeStart: number, - timeEnd: number, -): { value: boolean; warnings: { start?: string; end?: string; duration?: string } } => { - const validate = { value: true, warnings: { start: '', end: '', duration: '' } }; - - const { start, end } = handleTimeEntry(field, value, timeStart, timeEnd); - - if (end < start) { - validate.warnings.start = 'Start time later than end time'; - } - - return validate; -}; diff --git a/apps/client/src/features/rundown/Rundown.module.scss b/apps/client/src/features/rundown/Rundown.module.scss index d56470e8c..a4822e1cd 100644 --- a/apps/client/src/features/rundown/Rundown.module.scss +++ b/apps/client/src/features/rundown/Rundown.module.scss @@ -2,7 +2,6 @@ height: calc(100% - 1.5rem); overflow: hidden; padding-top: 0.5rem; - flex: 1; } .eventContainer { diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 2d036c70b..54cd3cdb1 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -1,7 +1,7 @@ import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react'; import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { OntimeRundown, Playback, SupportedEvent } from 'ontime-types'; +import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, Playback, SupportedEvent } from 'ontime-types'; import { getFirst, getNext, getPrevious } from 'ontime-utils'; import { useEventAction } from '../../common/hooks/useEventAction'; @@ -38,7 +38,7 @@ export default function Rundown(props: RundownProps) { const viewFollowsCursor = appMode === AppMode.Run; const cursorRef = useRef(null); const scrollRef = useRef(null); - useFollowComponent({ followRef: cursorRef, scrollRef: scrollRef, doFollow: true }); + useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: true }); // DND KIT const sensors = useSensors(useSensor(PointerSensor)); @@ -66,8 +66,8 @@ export default function Rundown(props: RundownProps) { type: SupportedEvent.Event, }; const options = { - defaultPublic: defaultPublic, - startTimeIsLastEnd: startTimeIsLastEnd, + defaultPublic, + startTimeIsLastEnd, lastEventId: cursor, after: cursor, }; @@ -197,7 +197,7 @@ export default function Rundown(props: RundownProps) { return insertAtCursor(SupportedEvent.Event, null)} />; } - let previousEnd = 0; + let previousEnd: null | number = null; let thisEnd = 0; let previousEventId: string | undefined; let eventIndex = 0; @@ -213,10 +213,13 @@ export default function Rundown(props: RundownProps) { eventIndex = 0; } let isFirstEvent = false; - if (entry.type === SupportedEvent.Event) { + if (isOntimeEvent(entry)) { isFirstEvent = eventIndex === 0; + // event indexes are 1 based in frontend eventIndex++; - previousEnd = thisEnd; + if (!isFirstEvent) { + previousEnd = thisEnd; + } thisEnd = entry.timeEnd; previousEventId = entry.id; } @@ -236,7 +239,6 @@ export default function Rundown(props: RundownProps) { )} diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index b6e9ccf02..3263d9aee 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -2,12 +2,12 @@ import { useCallback } from 'react'; import { GetRundownCached, isOntimeEvent, + MaybeNumber, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, } from 'ontime-types'; -import { calculateDuration, getCueCandidate } from 'ontime-utils'; import { RUNDOWN } from '../../common/api/apiConstants'; import { useEventAction } from '../../common/hooks/useEventAction'; @@ -28,17 +28,16 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del interface RundownEntryProps { type: SupportedEvent; isPast: boolean; - isFirstEvent: boolean; data: OntimeRundownEntry; selected: boolean; eventIndex: number; hasCursor: boolean; next: boolean; - previousEnd: number; + previousEnd: MaybeNumber; previousEventId?: string; playback?: Playback; // we only care about this if this event is playing isRolling: boolean; // we need to know even if not related to this event - disableEdit: boolean; // we disable edit when the window is extracted + disableEdit: boolean; } export default function RundownEntry(props: RundownEntryProps) { @@ -53,7 +52,6 @@ export default function RundownEntry(props: RundownEntryProps) { playback, isRolling, disableEdit, - isFirstEvent, eventIndex, } = props; const { emitError } = useEmitLog(); @@ -111,9 +109,7 @@ export default function RundownEntry(props: RundownEntryProps) { } case 'clone': { const newEvent = cloneEvent(data as OntimeEvent, data.id); - const rundown = ontimeQueryClient.getQueryData(RUNDOWN)?.rundown ?? []; - newEvent.cue = getCueCandidate(rundown, data.id); - addEvent(newEvent); + addEvent(newEvent, { after: data.id }); break; } case 'update': { @@ -139,26 +135,6 @@ export default function RundownEntry(props: RundownEntryProps) { batchUpdateEvents(changes, eventIds); return clearSelectedEvents(); } - - if (field === 'durationOverride' && data.type === SupportedEvent.Event) { - // duration defines timeEnd - newData.duration = value as number; - newData.timeEnd = data.timeStart + (value as number); - return updateEvent(newData); - } - - if (field === 'timeStart' && data.type === SupportedEvent.Event) { - newData.duration = calculateDuration(value as number, data.timeEnd); - newData.timeStart = value as number; - return updateEvent(newData); - } - - if (field === 'timeEnd' && data.type === SupportedEvent.Event) { - newData.duration = calculateDuration(data.timeStart, value as number); - newData.timeEnd = value as number; - return updateEvent(newData); - } - if (field in data) { // @ts-expect-error not sure how to type this newData[field] = value; @@ -186,7 +162,7 @@ export default function RundownEntry(props: RundownEntryProps) { timerType={data.timerType} title={data.title} note={data.note} - delay={data.delay || 0} + delay={data.delay ?? 0} previousEnd={previousEnd} colour={data.colour} isPast={isPast} @@ -198,7 +174,6 @@ export default function RundownEntry(props: RundownEntryProps) { isRolling={isRolling} actionHandler={actionHandler} disableEdit={disableEdit} - isFirstEvent={isFirstEvent} /> ); } else if (data.type === SupportedEvent.Block) { diff --git a/apps/client/src/features/rundown/event-block/EventBlock.module.scss b/apps/client/src/features/rundown/event-block/EventBlock.module.scss index bbc41eaf4..592923630 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.module.scss +++ b/apps/client/src/features/rundown/event-block/EventBlock.module.scss @@ -10,7 +10,7 @@ $skip-opacity: 0.1; grid-template-areas: 'binder ... ... ...' 'binder pb-actions times actions' - 'binder pb-actions title next-ind' + 'binder pb-actions title next' 'binder pb-actions estatus estatus' 'binder ... ... ...'; @@ -53,7 +53,7 @@ $skip-opacity: 0.1; /* we stop the eventActions from having opacity to fix issue with dropdown drawing order */ &.past:not(.skip) { - .delayNote, + .timerNote, .statusElements, .eventTitle, .eventNote, @@ -68,7 +68,7 @@ $skip-opacity: 0.1; &.skip { border: 1px solid $white-3; - .delayNote, + .timerNote, .eventTitle, .eventNote, .binder, @@ -127,10 +127,12 @@ $skip-opacity: 0.1; gap: $block-clearance; height: 100%; - .delayNote { - font-size: 0.75rem; - line-height: 0.8rem; - color: $ontime-delay-text; + .timerNote { + display: grid; + place-content: center; + color: $blue-500; + margin-right: 0.5em; + font-size: 1.5em; } } @@ -189,7 +191,7 @@ $skip-opacity: 0.1; } .nextTag { - grid-area: next-ind; + grid-area: next; font-size: 1rem; color: $orange-500; letter-spacing: 0.03px; @@ -197,31 +199,6 @@ $skip-opacity: 0.1; text-align: right; } -.indicators { - grid-area: next-ind; - display: flex; - justify-content: flex-end; - align-items: center; - gap: 0.5rem; - - .indicator { - background-color: transparent; - margin: 0.4rem; - margin-right: 0; - border-radius: 0.7rem; - width: 0.7rem; - height: 0.7rem; - } - .indicator.delay { - background-color: $ontime-delay; - } - .indicator.nextDay, - .indicator.overlap, - .indicator.spacing { - background-color: $gray-600; - } -} - .eventStatus { grid-area: status; display: flex; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index 6e6c10b3e..6d784d98c 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -7,7 +7,7 @@ import { IoPeople } from '@react-icons/all-files/io5/IoPeople'; import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical'; -import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types'; +import { EndAction, MaybeNumber, OntimeEvent, Playback, TimerType } from 'ontime-types'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import useRundown from '../../../common/hooks-query/useRundown'; @@ -19,6 +19,7 @@ import { useEventIdSwapping } from '../useEventIdSwapping'; import { EditMode, useEventSelection } from '../useEventSelection'; import EventBlockInner from './EventBlockInner'; +import RundownIndicators from './RundownIndicators'; import style from './EventBlock.module.scss'; @@ -47,7 +48,7 @@ interface EventBlockProps { title: string; note: string; delay: number; - previousEnd: number; + previousEnd: MaybeNumber; colour: string; isPast: boolean; next: boolean; @@ -66,7 +67,6 @@ interface EventBlockProps { }, ) => void; disableEdit: boolean; - isFirstEvent: boolean; } export default function EventBlock(props: EventBlockProps) { @@ -94,7 +94,6 @@ export default function EventBlock(props: EventBlockProps) { isRolling, actionHandler, disableEdit, - isFirstEvent, } = props; const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping(); const { selectedEvents, setSelectedEvents } = useEventSelection(); @@ -253,12 +252,15 @@ export default function EventBlock(props: EventBlockProps) { onContextMenu={onContextMenu} id='event-block' > + +
{cue}
+ {isVisible && ( )} diff --git a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx index 5ac6f619f..58af01215 100644 --- a/apps/client/src/features/rundown/event-block/EventBlockInner.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlockInner.tsx @@ -11,10 +11,8 @@ import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward' import { IoStop } from '@react-icons/all-files/io5/IoStop'; import { IoTime } from '@react-icons/all-files/io5/IoTime'; import { EndAction, Playback, TimerType } from 'ontime-types'; -import { millisToString } from 'ontime-utils'; import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn'; -import { millisToDelayString } from '../../../common/utils/dateConfig'; import { tooltipDelayMid } from '../../../ontimeConfig'; import EditableBlockTitle from '../common/EditableBlockTitle'; import { EventItemActions } from '../RundownEntry'; @@ -47,7 +45,6 @@ interface EventBlockInnerProps { title: string; note: string; delay: number; - previousEnd: number; next: boolean; skip: boolean; selected: boolean; @@ -55,7 +52,6 @@ interface EventBlockInnerProps { isRolling: boolean; actionHandler: (action: EventItemActions, payload?: any) => void; disableEdit: boolean; - isFirstEvent: boolean; } const EventBlockInner = (props: EventBlockInnerProps) => { @@ -70,7 +66,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => { title, note, delay, - previousEnd, next, skip = false, selected, @@ -78,7 +73,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => { isRolling, actionHandler, disableEdit, - isFirstEvent, } = props; const [renderInner, setRenderInner] = useState(false); @@ -111,59 +105,14 @@ const EventBlockInner = (props: EventBlockInnerProps) => { playBtnStyles._hover = {}; } - const delayedStart = Math.max(0, timeStart + delay); - const newTime = millisToString(delayedStart); - const delayTime = delay !== 0 ? millisToDelayString(delay) : null; - - const overlap = previousEnd - timeStart; - const overlapTime = !isFirstEvent - ? overlap > 0 - ? `Overlapping ${millisToDelayString(overlap)}` - : overlap < 0 - ? `Spacing ${millisToDelayString(overlap)}` - : null - : null; - return !renderInner ? null : ( <> - + - {next ? ( + {next && ( UP NEXT - ) : ( - - {delayTime && ( - - {delayTime}
- New Time: {newTime} - - } - > -
- - )} - {overlapTime && ( - -
0 ? style.overlap : style.spacing}`} /> - - )} - {timeStart > timeEnd && ( - -
- - )} - )} 0 ? 'Overlap' : 'Gap'}: ${overlapString}`; +} + +export default function RundownIndicators(props: RundownIndicatorProps) { + const { timeStart, previousEnd, delay } = props; + + const hasOverlap = formatOverlap(previousEnd, timeStart); + const hasDelay = formatDelay(timeStart, delay); + + return ( +
+ {hasDelay &&
{hasDelay}
} + {hasOverlap &&
{hasOverlap}
} +
+ ); +} diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx index 3e350e1f7..0ee705d08 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx @@ -1,10 +1,12 @@ import { memo } from 'react'; +import { Tooltip } from '@chakra-ui/react'; +import { IoAlertCircleOutline } from '@react-icons/all-files/io5/IoAlertCircleOutline'; import { OntimeEvent } from 'ontime-types'; -import { calculateDuration, millisToString } from 'ontime-utils'; import TimeInputWithButton from '../../../../common/components/input/time-input/TimeInputWithButton'; import { useEventAction } from '../../../../common/hooks/useEventAction'; -import { millisToDelayString } from '../../../../common/utils/dateConfig'; +import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; +import { tooltipDelayFast } from '../../../../ontimeConfig'; import style from '../EventBlock.module.scss'; @@ -14,73 +16,64 @@ interface EventBlockTimerProps { timeEnd: number; duration: number; delay: number; - previousEnd: number; } -type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timeWarning' | 'timeDanger'; +type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride'; // we call it durationOverride to stop from passing as a duration value const EventBlockTimers = (props: EventBlockTimerProps) => { - const { eventId, timeStart, timeEnd, duration, delay, previousEnd } = props; - const { updateEvent } = useEventAction(); + const { eventId, timeStart, timeEnd, duration, delay } = props; + const { updateEvent, updateTimer } = useEventAction(); - const handleSubmit = (field: TimeActions, value: number) => { - const newEventData: Partial = { id: eventId }; - switch (field) { - case 'durationOverride': { - // duration defines timeEnd - newEventData.duration = value; - newEventData.timeEnd = timeStart + value; - break; - } - case 'timeStart': { - newEventData.duration = calculateDuration(value, timeEnd); - newEventData.timeStart = value; - break; - } - case 'timeEnd': { - newEventData.duration = calculateDuration(timeStart, value); - newEventData.timeEnd = value; - break; - } + // In sync with EventEditorTimes + const handleSubmit = (field: TimeActions, value: string) => { + if (field === 'timeStart' || field === 'timeEnd') { + updateTimer(eventId, field, value); + return; + } + + if (field === 'durationOverride') { + const timeInMillis = forgivingStringToMillis(value); + const newEventData: Partial = { id: eventId, timeEnd: timeStart + timeInMillis }; + updateEvent(newEventData); + return; } - updateEvent(newEventData); }; - const delayedStart = Math.max(0, timeStart + delay); - const newTime = millisToString(delayedStart); - const delayTime = delay !== 0 ? millisToDelayString(delay) : null; + const overMidnight = timeStart > timeEnd; + const hasDelay = delay !== 0; return (
- name='timeStart' submitHandler={handleSubmit} time={timeStart} - delay={delay} + hasDelay={hasDelay} placeholder='Start' - previousEnd={previousEnd} /> - name='timeEnd' submitHandler={handleSubmit} time={timeEnd} - delay={delay} + hasDelay={hasDelay} placeholder='End' - previousEnd={previousEnd} /> - name='durationOverride' submitHandler={handleSubmit} time={duration} - delay={0} placeholder='Duration' - previousEnd={previousEnd} /> - {delayTime && ( -
- {delayTime} -
- {`New start: ${newTime}`} + {overMidnight && ( +
+ + +
)}
diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.tsx b/apps/client/src/features/rundown/event-editor/EventEditor.tsx index dc8476a1b..3fd979d4c 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/event-editor/EventEditor.tsx @@ -61,7 +61,7 @@ export default function EventEditor() { ); if (!event) { - return Loading...; + return Loading...; } // Compositing user fields by hand @@ -80,7 +80,7 @@ export default function EventEditor() { }; return ( -
+
HEADER ACTIONS?
{ const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType, timeWarning, timeDanger } = props; - const { updateEvent } = useEventAction(); + const { updateEvent, updateTimer } = useEventAction(); - const handleSubmit = (field: TimeActions, value: number | string | boolean) => { - const newEventData: Partial = { id: eventId }; - switch (field) { - case 'durationOverride': { - // duration defines timeEnd - newEventData.duration = value as number; - newEventData.timeEnd = timeStart + (value as number); - break; - } - case 'timeStart': { - newEventData.duration = calculateDuration(value as number, timeEnd); - newEventData.timeStart = value as number; - break; - } - case 'timeEnd': { - newEventData.duration = calculateDuration(timeStart, value as number); - newEventData.timeEnd = value as number; - break; - } - case 'isPublic': { - updateEvent({ id: eventId, isPublic: !(value as boolean) }); - break; - } - default: { - if (field === 'timerType' || field === 'endAction' || field === 'timeWarning' || field === 'timeDanger') { - // @ts-expect-error -- not sure how to typecheck here - newEventData[field as keyof OntimeEvent] = value as string; - } else { - return; - } - } + // In sync with EventBlockTimers + const handleTimeSubmit = (field: TimeActions, value: string) => { + if (field === 'timeStart' || field === 'timeEnd') { + updateTimer(eventId, field, value); + return; + } + + if (field === 'durationOverride') { + const timeInMillis = forgivingStringToMillis(value); + const newEventData: Partial = { id: eventId, timeEnd: timeStart + timeInMillis }; + updateEvent(newEventData); + return; } - updateEvent(newEventData); }; - const delayTime = delay !== 0 ? millisToDelayString(delay) : null; - const startLabel = delayTime ? `New start ${millisToString(timeStart + delay)}` : 'Start time'; - const endLabel = delayTime ? `New end ${millisToString(timeEnd + delay)}` : 'End time'; - const inputTimeLabels = cx([style.inputLabel, delayTime ? style.delayLabel : null]); + const handleSubmit = (field: HandledActions, value: string | boolean) => { + if (field === 'isPublic') { + updateEvent({ id: eventId, isPublic: !(value as boolean) }); + return; + } + + if (field === 'timeWarning' || field === 'timeDanger') { + const newTime = forgivingStringToMillis(value as string); + updateEvent({ id: eventId, [field]: newTime }); + return; + } + + if (field === 'timerType' || field === 'endAction') { + updateEvent({ id: eventId, [field]: value }); + return; + } + }; + + const hasDelay = delay !== 0; + const delayTime = hasDelay ? millisToDelayString(delay) : null; + const startLabel = delayTime ? `New Start ${millisToString(timeStart + delay)}` : 'Start time'; + const endLabel = delayTime ? `New End ${millisToString(timeEnd + delay)}` : 'End time'; + const inputTimeLabels = cx([style.inputLabel, hasDelay ? style.delayLabel : null]); return (
@@ -87,11 +79,10 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { {startLabel}
@@ -100,11 +91,10 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { {endLabel}
@@ -113,9 +103,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { Duration @@ -123,18 +112,14 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
- -
- Event visibility + Event Visibility