refactor: update timers (#729)

* refactor: remove duplication

* refactor: previous times

* refactor: event patching
This commit is contained in:
Carlos Valente
2024-01-25 13:36:45 +01:00
committed by GitHub
parent e748bff0dd
commit 74e1ae609c
32 changed files with 448 additions and 555 deletions
@@ -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>
@@ -30,7 +30,7 @@ export default function RenameClientModal({ isOpen, onClose }: RenameClientModal
const handleRename = async () => {
if (newName) {
await setClientName(newName);
setClientName(newName);
persistName(newName);
onClose();
}
+54 -19
View File
@@ -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;
};