mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
refactor: update timers (#729)
* refactor: remove duplication * refactor: previous times * refactor: event patching
This commit is contained in:
@@ -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<T extends string> {
|
||||
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<T extends string>(props: TimeInputProps<T>) {
|
||||
const { name, submitHandler, time = 0, placeholder, className } = props;
|
||||
const { emitError } = useEmitLog();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [value, setValue] = useState<string>('');
|
||||
@@ -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) {
|
||||
<Input
|
||||
size='sm'
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
data-testid={`time-input-${name}`}
|
||||
className={timeInputClass}
|
||||
type='text'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
$input-font-size: 15px;
|
||||
$input-delayed-border-color: #E69056;
|
||||
$input-delayed-border-color: $ontime-delay-text;
|
||||
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
width: fit-content;
|
||||
|
||||
.inputLeft {
|
||||
max-width: fit-content;
|
||||
&.delayed {
|
||||
border: 1px solid $input-delayed-border-color;
|
||||
}
|
||||
|
||||
.inputLeft,
|
||||
@@ -20,16 +20,7 @@ $input-delayed-border-color: #E69056;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
.warn {
|
||||
&::after {
|
||||
content: "*";
|
||||
color: $warning-orange;
|
||||
}
|
||||
}
|
||||
|
||||
&.delayed {
|
||||
.inputField {
|
||||
border: 1px solid $input-delayed-border-color;
|
||||
}
|
||||
.inputButton {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,39 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||
import { cx } from '../../../utils/styleUtils';
|
||||
import { TimeEntryField } from '../../../utils/timesManager';
|
||||
|
||||
import TimeInput from './TimeInput';
|
||||
|
||||
import style from './TimeInputWithButton.module.scss';
|
||||
|
||||
interface TimeInputProps {
|
||||
id?: TimeEntryField;
|
||||
name: TimeEntryField;
|
||||
submitHandler: (field: TimeEntryField, value: number) => void;
|
||||
interface TimeInputWithButtonProps<T extends string> {
|
||||
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<T extends string>(props: TimeInputWithButtonProps<T>) {
|
||||
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 (
|
||||
<InputGroup size='sm' className={inputClasses}>
|
||||
<InputLeftElement className={style.inputLeft}>
|
||||
<Tooltip label={TooltipLabel} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ontime-subtle-white'
|
||||
className={buttonClasses}
|
||||
tabIndex={-1}
|
||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
||||
borderRight='1px solid transparent'
|
||||
borderRadius='2px 0 0 2px'
|
||||
>
|
||||
{ButtonText}
|
||||
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button size='sm' variant='ontime-subtle-white' className={style.inputButton} tabIndex={-1}>
|
||||
{placeholder.charAt(0)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<TimeInput
|
||||
id={id}
|
||||
<TimeInput<T>
|
||||
name={name}
|
||||
submitHandler={submitHandler}
|
||||
time={time}
|
||||
delay={delay}
|
||||
placeholder={placeholder}
|
||||
previousEnd={previousEnd}
|
||||
className={style.inputField}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal
|
||||
|
||||
const handleRename = async () => {
|
||||
if (newName) {
|
||||
await setClientName(newName);
|
||||
setClientName(newName);
|
||||
persistName(newName);
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -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<GetRundownCached>(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<GetRundownCached>(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<GetRundownCached>(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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user