mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
committed by
GitHub
parent
40bede200c
commit
1a8b35a5ea
@@ -17,9 +17,8 @@ interface TimeInputProps {
|
||||
time?: number;
|
||||
delay?: number;
|
||||
placeholder: string;
|
||||
validationHandler: (entry: TimeEntryField, val: number) => boolean;
|
||||
previousEnd?: number;
|
||||
warning?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
function ButtonInitial(name: TimeEntryField) {
|
||||
@@ -29,25 +28,15 @@ function ButtonInitial(name: TimeEntryField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
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}` : ''}`;
|
||||
function ButtonTooltip(name: TimeEntryField, tooltip?: string) {
|
||||
if (name === 'timeStart') return `Start${tooltip ? `: ${tooltip}` : ''}`;
|
||||
if (name === 'timeEnd') return `End${tooltip ? `: ${tooltip}` : ''}`;
|
||||
if (name === 'durationOverride') return `Duration${tooltip ? `: ${tooltip}` : ''}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function TimeInput(props: TimeInputProps) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
submitHandler,
|
||||
time = 0,
|
||||
delay = 0,
|
||||
placeholder,
|
||||
validationHandler,
|
||||
previousEnd = 0,
|
||||
warning,
|
||||
} = props;
|
||||
const { id, name, submitHandler, time = 0, delay = 0, placeholder, previousEnd = 0 } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState<string>('');
|
||||
@@ -103,15 +92,12 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
// check if time is different from before
|
||||
if (newValMillis === time) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
},
|
||||
[name, previousEnd, submitHandler, time, validationHandler],
|
||||
[name, previousEnd, submitHandler, time],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -171,11 +157,11 @@ export default function TimeInput(props: TimeInputProps) {
|
||||
|
||||
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 buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null]);
|
||||
|
||||
const TooltipLabel = useMemo(() => {
|
||||
return ButtonTooltip(name, warning);
|
||||
}, [name, warning]);
|
||||
return ButtonTooltip(name, '');
|
||||
}, [name]);
|
||||
|
||||
const ButtonText = useMemo(() => {
|
||||
return ButtonInitial(name);
|
||||
|
||||
@@ -1,44 +1 @@
|
||||
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { Select, Switch } from '@chakra-ui/react';
|
||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||
import { calculateDuration, millisToString } from 'ontime-utils';
|
||||
@@ -7,7 +7,6 @@ import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||
import { millisToDelayString } from '../../../common/utils/dateConfig';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
@@ -29,13 +28,6 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType } = props;
|
||||
const { updateEvent } = useEventAction();
|
||||
|
||||
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
|
||||
|
||||
const timerValidationHandler = (entry: TimeEntryField, val: number) => {
|
||||
const valid = validateEntry(entry, val, timeStart, timeEnd);
|
||||
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
const handleSubmit = (field: TimeActions, value: number | string | boolean) => {
|
||||
const newEventData: Partial<OntimeEvent> = { id: eventId };
|
||||
@@ -87,11 +79,9 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
id='timeStart'
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
placeholder='Start'
|
||||
warning={warning.start}
|
||||
/>
|
||||
<label className={inputTimeLabels} htmlFor='timeEnd'>
|
||||
{endLabel}
|
||||
@@ -100,11 +90,9 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
id='timeEnd'
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
placeholder='End'
|
||||
warning={warning.end}
|
||||
/>
|
||||
<label className={style.inputLabel} htmlFor='durationOverride'>
|
||||
Duration
|
||||
@@ -113,10 +101,8 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
|
||||
id='durationOverride'
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={timerValidationHandler}
|
||||
time={duration}
|
||||
placeholder='Duration'
|
||||
warning={warning.duration}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.timeSettings}>
|
||||
|
||||
@@ -198,7 +198,9 @@ export default function Rundown(props: RundownProps) {
|
||||
if (index === 0) {
|
||||
eventIndex = 0;
|
||||
}
|
||||
let isFirstEvent = false;
|
||||
if (entry.type === SupportedEvent.Event) {
|
||||
isFirstEvent = eventIndex === 0;
|
||||
eventIndex++;
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = entry.timeEnd;
|
||||
@@ -220,6 +222,7 @@ export default function Rundown(props: RundownProps) {
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
isPast={isPast}
|
||||
isFirstEvent={isFirstEvent}
|
||||
data={entry}
|
||||
selected={isSelected}
|
||||
hasCursor={hasCursor}
|
||||
|
||||
@@ -19,6 +19,7 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del
|
||||
interface RundownEntryProps {
|
||||
type: SupportedEvent;
|
||||
isPast: boolean;
|
||||
isFirstEvent: boolean;
|
||||
data: OntimeRundownEntry;
|
||||
selected: boolean;
|
||||
hasCursor: boolean;
|
||||
@@ -31,8 +32,19 @@ interface RundownEntryProps {
|
||||
}
|
||||
|
||||
export default function RundownEntry(props: RundownEntryProps) {
|
||||
const { isPast, data, selected, hasCursor, next, previousEnd, previousEventId, playback, isRolling, disableEdit } =
|
||||
props;
|
||||
const {
|
||||
isPast,
|
||||
data,
|
||||
selected,
|
||||
hasCursor,
|
||||
next,
|
||||
previousEnd,
|
||||
previousEventId,
|
||||
playback,
|
||||
isRolling,
|
||||
disableEdit,
|
||||
isFirstEvent,
|
||||
} = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const { addEvent, updateEvent, deleteEvent, swapEvents } = useEventAction();
|
||||
|
||||
@@ -100,7 +112,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
}
|
||||
case 'clone': {
|
||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? []
|
||||
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
|
||||
newEvent.cue = getCueCandidate(rundown, data.id);
|
||||
addEvent(newEvent);
|
||||
break;
|
||||
@@ -176,6 +188,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
isRolling={isRolling}
|
||||
actionHandler={actionHandler}
|
||||
disableEdit={disableEdit}
|
||||
isFirstEvent={isFirstEvent}
|
||||
/>
|
||||
);
|
||||
} else if (data.type === SupportedEvent.Block) {
|
||||
|
||||
@@ -10,11 +10,11 @@ $skip-opacity: 0.1;
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"binder ... ... ..."
|
||||
"binder pb-actions times actions"
|
||||
"binder pb-actions title next"
|
||||
"binder pb-actions estatus estatus"
|
||||
"binder ... ... ...";
|
||||
'binder ... ... ...'
|
||||
'binder pb-actions times actions'
|
||||
'binder pb-actions title next-ind'
|
||||
'binder pb-actions estatus estatus'
|
||||
'binder ... ... ...';
|
||||
|
||||
grid-template-columns: $block-binder-width auto 1fr auto;
|
||||
grid-template-rows: 0.25rem 2.25rem 2.25rem 2.25rem 0.25rem;
|
||||
@@ -170,8 +170,8 @@ $skip-opacity: 0.1;
|
||||
grid-area: estatus;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"notes status"
|
||||
"progb progb";
|
||||
'notes status'
|
||||
'progb progb';
|
||||
gap: 2px;
|
||||
grid-template-rows: auto 0.25rem;
|
||||
align-items: center;
|
||||
@@ -190,9 +190,8 @@ $skip-opacity: 0.1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
||||
.nextTag {
|
||||
grid-area: next;
|
||||
grid-area: next-ind;
|
||||
font-size: 1rem;
|
||||
color: $orange-500;
|
||||
letter-spacing: 0.03px;
|
||||
@@ -200,6 +199,35 @@ $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: $gray-1250;
|
||||
margin: 0.4rem;
|
||||
margin-right: 0;
|
||||
border-radius: 0.7rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
}
|
||||
.indicator.delay {
|
||||
background-color: $ontime-delay;
|
||||
}
|
||||
.indicator.overlap {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
.indicator.spacing {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
.indicator.nextDay {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.eventStatus {
|
||||
grid-area: status;
|
||||
display: flex;
|
||||
@@ -208,7 +236,6 @@ $skip-opacity: 0.1;
|
||||
gap: 0.5rem;
|
||||
color: var(--status-color-override, $gray-500);
|
||||
|
||||
|
||||
.statusIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
@@ -50,6 +50,7 @@ interface EventBlockProps {
|
||||
},
|
||||
) => void;
|
||||
disableEdit: boolean;
|
||||
isFirstEvent: boolean;
|
||||
}
|
||||
|
||||
export default function EventBlock(props: EventBlockProps) {
|
||||
@@ -76,6 +77,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
isRolling,
|
||||
actionHandler,
|
||||
disableEdit,
|
||||
isFirstEvent,
|
||||
} = props;
|
||||
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
|
||||
const moveCursorTo = useAppMode((state) => state.setCursor);
|
||||
@@ -210,6 +212,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
isRolling={isRolling}
|
||||
actionHandler={actionHandler}
|
||||
disableEdit={disableEdit}
|
||||
isFirstEvent={isFirstEvent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,11 @@ 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 { useAppMode } from '../../../common/stores/appModeStore';
|
||||
import { millisToDelayString } from '../../../common/utils/dateConfig';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
import { EventItemActions } from '../RundownEntry';
|
||||
@@ -53,6 +55,7 @@ interface EventBlockInnerProps {
|
||||
isRolling: boolean;
|
||||
actionHandler: (action: EventItemActions, payload?: any) => void;
|
||||
disableEdit: boolean;
|
||||
isFirstEvent: boolean;
|
||||
}
|
||||
|
||||
const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
@@ -76,6 +79,7 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
isRolling,
|
||||
actionHandler,
|
||||
disableEdit,
|
||||
isFirstEvent,
|
||||
} = props;
|
||||
|
||||
const [renderInner, setRenderInner] = useState(false);
|
||||
@@ -103,6 +107,19 @@ 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 : (
|
||||
<>
|
||||
<EventBlockTimers
|
||||
@@ -114,10 +131,36 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
|
||||
{next && (
|
||||
{next ? (
|
||||
<Tooltip label='Next event' {...tooltipProps}>
|
||||
<span className={style.nextTag}>UP NEXT</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={style.indicators}>
|
||||
<Tooltip
|
||||
label={
|
||||
delayTime && (
|
||||
<div>
|
||||
{delayTime}
|
||||
<br />
|
||||
New Time: {newTime}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={`${style.indicator} ${delayTime ? style.delay : ''}`} />
|
||||
</Tooltip>
|
||||
<Tooltip label={overlapTime}>
|
||||
<div
|
||||
className={`${style.indicator} ${
|
||||
overlap > 0 ? style.overlap : overlap < 0 && overlapTime !== null ? style.spacing : ''
|
||||
}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Start time is later than end'>
|
||||
<div className={`${style.indicator} ${timeStart > timeEnd ? style.nextDay : ''}`} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
<EventBlockPlayback
|
||||
eventId={eventId}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { calculateDuration, millisToString } from 'ontime-utils';
|
||||
|
||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||
import { useEventAction } from '../../../../common/hooks/useEventAction';
|
||||
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||
import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
|
||||
|
||||
import style from '../EventBlock.module.scss';
|
||||
|
||||
@@ -24,8 +23,6 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
|
||||
const { eventId, timeStart, timeEnd, duration, delay, previousEnd } = props;
|
||||
const { updateEvent } = useEventAction();
|
||||
|
||||
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
|
||||
|
||||
const handleSubmit = (field: TimeActions, value: number) => {
|
||||
const newEventData: Partial<OntimeEvent> = { id: eventId };
|
||||
switch (field) {
|
||||
@@ -49,21 +46,6 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
|
||||
updateEvent(newEventData);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidation = useCallback(
|
||||
(field: TimeEntryField, value: number) => {
|
||||
const valid = validateEntry(field, value, timeStart, timeEnd);
|
||||
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
|
||||
return valid.value;
|
||||
},
|
||||
[timeEnd, timeStart],
|
||||
);
|
||||
|
||||
const delayedStart = Math.max(0, timeStart + delay);
|
||||
const newTime = millisToString(delayedStart);
|
||||
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
|
||||
@@ -73,32 +55,26 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
|
||||
<TimeInput
|
||||
name='timeStart'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
placeholder='Start'
|
||||
previousEnd={previousEnd}
|
||||
warning={warning.start}
|
||||
/>
|
||||
<TimeInput
|
||||
name='timeEnd'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
placeholder='End'
|
||||
previousEnd={previousEnd}
|
||||
warning={warning.end}
|
||||
/>
|
||||
<TimeInput
|
||||
name='durationOverride'
|
||||
submitHandler={handleSubmit}
|
||||
validationHandler={handleValidation}
|
||||
time={duration}
|
||||
delay={0}
|
||||
placeholder='Duration'
|
||||
previousEnd={previousEnd}
|
||||
warning={warning.duration}
|
||||
/>
|
||||
{delayTime && (
|
||||
<div className={style.delayNote}>
|
||||
|
||||
Reference in New Issue
Block a user