refactor: UI for linking events (#763)

This commit is contained in:
Carlos Valente
2024-02-11 15:18:04 +01:00
committed by GitHub
parent 12f81c63ce
commit 5355e45b80
49 changed files with 1353 additions and 596 deletions
@@ -17,7 +17,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
if (typeof delayValue === 'number') {
if (delayValue < 0) {
return (
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
<span className={style.delaySymbol}>
<IoChevronDown />
</span>
@@ -27,7 +27,7 @@ export default function DelayIndicator(props: DelayIndicatorProps) {
if (delayValue > 0) {
return (
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue)}>
<Tooltip openDelay={tooltipDelayFast} label={millisToDelayString(delayValue, true)}>
<span className={style.delaySymbol}>
<IoChevronUp />
</span>
@@ -1,8 +1,4 @@
$input-font-size: 15px;
$input-delayed-border-color: #E69056;
.timeInput {
font-size: $input-font-size;
letter-spacing: 1px;
width: 6.5em;
}
}
@@ -4,6 +4,7 @@ import { millisToString } from 'ontime-utils';
import { useEmitLog } from '../../../stores/logger';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import { cx } from '../../../utils/styleUtils';
import style from './TimeInput.module.scss';
interface TimeInputProps<T extends string> {
@@ -11,11 +12,12 @@ interface TimeInputProps<T extends string> {
submitHandler: (field: T, value: string) => void;
time?: number;
placeholder: string;
disabled?: boolean;
className?: string;
}
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
const { name, submitHandler, time = 0, placeholder, className } = props;
const { name, submitHandler, time = 0, placeholder, disabled, className } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
@@ -118,14 +120,16 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
resetValue();
}, [resetValue, time]);
const timeInputClass = className ? className : style.timeInput;
const timeInputClasses = cx([style.timeInput, className]);
return (
<Input
disabled={disabled}
size='sm'
ref={inputRef}
data-testid={`time-input-${name}`}
className={timeInputClass}
className={timeInputClasses}
fontSize='1rem'
type='text'
placeholder={placeholder}
variant='ontime-filled'
@@ -1,24 +1,12 @@
$input-font-size: 1rem;
$input-delayed-border-color: $ontime-delay-text;
.timeInput {
border: 1px solid transparent;
&.delayed {
border: 1px solid $input-delayed-border-color;
}
.inputLeft,
.inputButton {
aspect-ratio: 1;
}
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
width: 7.5em;
padding: 0 0 0 2.6em;
}
.inputButton {
border-radius: 2px 0 0 2px;
}
}
.inputField {
max-width: 7.75em;
}
@@ -1,4 +1,5 @@
import { Button, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { PropsWithChildren } from 'react';
import { InputGroup, Tooltip } from '@chakra-ui/react';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import { cx } from '../../../utils/styleUtils';
@@ -12,30 +13,28 @@ interface TimeInputWithButtonProps<T extends string> {
submitHandler: (field: T, value: string) => void;
time?: number;
hasDelay?: boolean;
disabled?: boolean;
placeholder: string;
}
export default function TimeInputWithButton<T extends string>(props: TimeInputWithButtonProps<T>) {
const { name, submitHandler, time, hasDelay, placeholder } = props;
export default function TimeInputWithButton<T extends string>(props: PropsWithChildren<TimeInputWithButtonProps<T>>) {
const { name, submitHandler, time, hasDelay, placeholder, disabled, children } = props;
const inputClasses = cx([style.timeInput, hasDelay ? style.delayed : null]);
return (
<InputGroup size='sm' className={inputClasses} width='fit-content'>
<InputLeftElement className={style.inputLeft}>
<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<T>
name={name}
submitHandler={submitHandler}
time={time}
placeholder={placeholder}
className={style.inputField}
/>
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
<TimeInput<T>
name={name}
submitHandler={submitHandler}
time={time}
placeholder={placeholder}
className={style.inputField}
disabled={disabled}
/>
</Tooltip>
{children}
</InputGroup>
);
}
+42 -2
View File
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { isOntimeEvent, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
import { reorderArray, swapEventData } from 'ontime-utils';
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
import { RUNDOWN } from '../api/apiConstants';
import { logAxiosError } from '../api/apiUtils';
@@ -186,6 +186,7 @@ export const useEventAction = () => {
// check for adding time keyword
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
// TODO: is this logic solid?
const remainingString = value.substring(1);
newValMillis = getPreviousEnd() + forgivingStringToMillis(remainingString);
} else {
@@ -205,6 +206,44 @@ export const useEventAction = () => {
[_updateEventMutation, queryClient],
);
/**
* Toggles link of an event to the previous
*/
const linkTimer = useCallback(
async (eventId: string, linkStart: MaybeString) => {
let newEvent: Partial<OntimeEvent> = { id: eventId };
if (!linkStart) {
newEvent.linkStart = null;
} else {
const cachedRundown = queryClient.getQueryData<RundownCached>(RUNDOWN);
if (!cachedRundown) {
return;
}
const currentEvent = cachedRundown.rundown[eventId] as OntimeEvent;
if (!isOntimeEvent(currentEvent)) {
return;
}
const { previousEvent } = getPreviousEventNormal(cachedRundown.rundown, cachedRundown.order, eventId);
if (!previousEvent) {
newEvent.linkStart = null;
} else {
newEvent.linkStart = previousEvent.id;
const timePatch = getLinkedTimes(currentEvent, previousEvent);
newEvent = { ...newEvent, ...timePatch };
}
}
try {
await _updateEventMutation.mutateAsync(newEvent);
} catch (error) {
logAxiosError('Error updating event', error);
}
},
[_updateEventMutation, queryClient],
);
/**
* Calls mutation to edit multiple events
* @private
@@ -509,6 +548,7 @@ export const useEventAction = () => {
batchUpdateEvents,
deleteEvent,
deleteAllEvents,
linkTimer,
reorderEvent,
swapEvents,
updateEvent,
@@ -1,7 +1,4 @@
import {
forgivingStringToMillis,
millisToDelayString,
} from '../dateConfig';
import { forgivingStringToMillis, millisToDelayString } from '../dateConfig';
describe('test forgivingStringToMillis()', () => {
describe('function handles time with no separators', () => {
@@ -270,10 +267,10 @@ describe('millisToDelayString()', () => {
});
describe('converts values in seconds', () => {
it('shows a simple string with value in seconds', () => {
expect(millisToDelayString(10000)).toBe('+10 sec');
expect(millisToDelayString(10000, true)).toBe('+10 sec');
});
it('... and its negative counterpart', () => {
expect(millisToDelayString(-10000)).toBe('-10 sec');
expect(millisToDelayString(-10000, true)).toBe('-10 sec');
});
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
@@ -287,32 +284,32 @@ describe('millisToDelayString()', () => {
describe('converts values in minutes', () => {
it('shows a simple string with value in minutes', () => {
expect(millisToDelayString(720000)).toBe('+12 min');
expect(millisToDelayString(720000, true)).toBe('+12 min');
});
it('... and its negative counterpart', () => {
expect(millisToDelayString(-720000)).toBe('-12 min');
expect(millisToDelayString(-720000, true)).toBe('-12 min');
});
it('shows a simple string with value in minutes and seconds', () => {
expect(millisToDelayString(630000)).toBe('+00:10:30');
expect(millisToDelayString(630000, true)).toBe('+00:10:30');
});
it('... and its negative counterpart', () => {
expect(millisToDelayString(-630000)).toBe('-00:10:30');
expect(millisToDelayString(-630000, true)).toBe('-00:10:30');
});
const underAnHour = [60000, 360000, 720000];
underAnHour.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
expect(millisToDelayString(value, true)?.endsWith('min')).toBe(true);
});
});
});
describe('converts values with full time string', () => {
it('positive added time', () => {
expect(millisToDelayString(45015000)).toBe('+12:30:15');
expect(millisToDelayString(45015000, true)).toBe('+12:30:15');
});
it('negative added time', () => {
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
expect(millisToDelayString(-45015000, true)).toBe('-12:30:15');
});
});
});
+6 -4
View File
@@ -155,19 +155,21 @@ export const forgivingStringToMillis = (value: string): number => {
return millis;
};
export function millisToDelayString(millis: number | null): undefined | string | null {
export function millisToDelayString(millis: number | null, small = false): undefined | string | null {
if (millis == null || millis === 0) {
return null;
}
const isNegative = millis < 0;
const absMillis = Math.abs(millis);
const delayed = small ? '+' : 'delayed by ';
const ahead = small ? '-' : 'ahead by ';
if (absMillis < MILLIS_PER_MINUTE) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')} sec`;
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 's')} sec`;
} else if (absMillis < MILLIS_PER_HOUR && absMillis % MILLIS_PER_MINUTE === 0) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')} min`;
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'm')} min`;
} else {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
return `${isNegative ? ahead : delayed}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
}
}
@@ -21,6 +21,8 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
duration: event.duration,
timeEnd: event.timeEnd,
timerType: event.timerType,
timeStrategy: event.timeStrategy,
linkStart: event.linkStart,
endAction: event.endAction,
isPublic: event.isPublic,
skip: event.skip,