refactor: performance tweaks to event editor

This commit is contained in:
cv
2023-04-06 11:00:28 +02:00
parent 4c268d531e
commit e77572c0b6
8 changed files with 239 additions and 197 deletions
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
import { TitleActions } from '../../../../features/event-editor/composite/EventEditorTitles';
import Swatch from './Swatch';
@@ -8,8 +8,8 @@ import style from './SwatchSelect.module.scss';
interface ColourInputProps {
value: string;
name: EventEditorSubmitActions;
handleChange: (newValue: EventEditorSubmitActions, name: string) => void;
name: TitleActions;
handleChange: (newValue: TitleActions, name: string) => void;
}
const colours = [
@@ -167,6 +167,7 @@ $playback-width: 450px;
background-color: $gray-1200;
padding: 8px;
border-left: 1px solid $white-10;
border-radius: 0 8px 0 0;
}
}
@@ -1,35 +1,23 @@
import { useCallback, useEffect, useState } from 'react';
import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import { useEffect, useState } from 'react';
import { OntimeEvent } from 'ontime-types';
import CopyTag from '../../common/components/copy-tag/CopyTag';
import SwatchSelect from '../../common/components/input/colour-input/SwatchSelect';
import TimeInput from '../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../common/hooks/useEventAction';
import useRundown from '../../common/hooks-query/useRundown';
import { useEventEditorStore } from '../../common/stores/eventEditor';
import { useEmitLog } from '../../common/stores/logger';
import { millisToMinutes } from '../../common/utils/dateConfig';
import getDelayTo from '../../common/utils/getDelayTo';
import { calculateDuration, TimeEntryField, validateEntry } from '../../common/utils/timesManager';
import CountedTextArea from './composite/CountedTextArea';
import CountedTextInput from './composite/CountedTextInput';
import EventEditorTimes from './composite/EventEditorTimes';
import EventEditorTitles from './composite/EventEditorTitles';
import style from './EventEditor.module.scss';
export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
export type EventEditorSubmitActions = keyof OntimeEvent;
// Todo: add previous end to TimeInput fields
export default function EventEditor() {
const { openId } = useEventEditorStore();
const { data } = useRundown();
const { emitError } = useEmitLog();
const { updateEvent } = useEventAction();
const [event, setEvent] = useState<OntimeEvent | null>(null);
const [delay, setDelay] = useState(0);
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
useEffect(() => {
if (!data || !openId) {
@@ -46,83 +34,10 @@ export default function EventEditor() {
}
}, [data, event, openId]);
const handleSubmit = useCallback(
(field: EventEditorSubmitActions, value: string | number) => {
if (event === null) {
return;
}
const newEventData: Partial<OntimeEvent> = { id: event.id };
switch (field) {
case 'durationOverride': {
// duration defines timeEnd
newEventData.duration = value as number;
newEventData.timeEnd = event.timeStart + (value as number);
break;
}
case 'timeStart': {
newEventData.duration = calculateDuration(value as number, event.timeEnd);
newEventData.timeStart = value as number;
break;
}
case 'timeEnd': {
newEventData.duration = calculateDuration(event.timeStart, value as number);
newEventData.timeEnd = value as number;
break;
}
default: {
if (field in event) {
// create object with new field
newEventData[field] = value;
break;
} else {
emitError(`Unknown field: ${field}`);
return;
}
}
}
updateEvent(newEventData);
},
[emitError, event, updateEvent],
);
const timerValidationHandler = useCallback(
(entry: TimeEntryField, val: number) => {
if (!event?.timeStart) {
return true;
}
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
return valid.value;
},
[event?.timeStart, event?.timeEnd],
);
const handleChange = useCallback(
(field: string, value: string) => {
updateEvent({ id: event.id, [field]: value });
},
[event, updateEvent],
);
const togglePublic = useCallback(
(currentValue: boolean) => {
if (!event) {
return;
}
updateEvent({ id: event.id, isPublic: !currentValue });
},
[event, updateEvent],
);
if (!event) {
return <span>Loading...</span>;
}
const delayed = delay !== 0;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
const newStart = delayed ? `New start ${millisToString(event.timeStart + delay)}` : null;
const newEnd = delayed ? `New end ${millisToString(event.timeEnd + delay)}` : null;
return (
<div className={style.eventEditor}>
<div className={style.eventInfo}>
@@ -132,103 +47,24 @@ export default function EventEditor() {
<div className={style.eventActions}>
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
</div>
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={style.inputLabel}>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeStart}
delay={delay}
placeholder='Start'
warning={warning.start}
/>
<label className={style.inputLabel}>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeEnd}
delay={delay}
placeholder='End'
warning={warning.end}
/>
<label className={style.inputLabel}>Duration</label>
<TimeInput
name='durationOverride'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.duration}
placeholder='Duration'
warning={warning.duration}
/>
</div>
<div className={style.timeSettings}>
<label className={style.inputLabel}>Timer Type</label>
<Select
size='sm'
name='timerType'
value={event.timerType}
onChange={(event) => handleChange('timerType', event.target.value)}
variant='ontime'
>
<option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option>
<option value={TimerType.Clock}>Clock</option>
</Select>
<label className={style.inputLabel}>End Action</label>
<Select
size='sm'
name='endAction'
value={event.endAction}
onChange={(event) => handleChange('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.Continue}>Continue</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
</Select>
<span className={style.spacer} />
<label className={`${style.inputLabel} ${style.publicToggle}`}>
<Switch isChecked={event.isPublic} onChange={() => togglePublic(event.isPublic)} variant='ontime' />
Event is public
</label>
</div>
</div>
<div className={style.titles}>
<div className={style.left}>
<CountedTextInput field='title' label='Title' initialValue={event.title} submitHandler={handleSubmit} />
<CountedTextInput
field='presenter'
label='Presenter'
initialValue={event.presenter}
submitHandler={handleSubmit}
/>
<CountedTextInput
field='subtitle'
label='Subtitle'
initialValue={event.subtitle}
submitHandler={handleSubmit}
/>
</div>
<div className={style.right}>
<div className={style.column}>
<label className={style.inputLabel}>Colour</label>
<div className={style.inline}>
<SwatchSelect name='colour' value={event.colour} handleChange={handleSubmit} />
</div>
</div>
<CountedTextArea field='note' label='Note' initialValue={event.note} submitHandler={handleSubmit} />
</div>
</div>
<EventEditorTimes
eventId={event.id}
timeStart={event.timeStart}
timeEnd={event.timeEnd}
duration={event.duration}
delay={delay}
isPublic={event.isPublic}
endAction={event.endAction}
timerType={event.timerType}
/>
<EventEditorTitles
eventId={event.id}
title={event.title}
presenter={event.presenter}
subtitle={event.subtitle}
note={event.note}
colour={event.colour}
/>
</div>
);
}
@@ -35,4 +35,3 @@ const EventEditorExport = () => {
};
export default memo(EventEditorExport);
@@ -2,15 +2,16 @@ import { useCallback } from 'react';
import { Textarea } from '@chakra-ui/react';
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
import { EventEditorSubmitActions } from '../EventEditor';
import { TitleActions } from './EventEditorTitles';
import style from '../EventEditor.module.scss';
interface CountedTextAreaProps {
field: EventEditorSubmitActions;
field: TitleActions;
label: string;
initialValue: string;
submitHandler: (field: EventEditorSubmitActions, value: string) => void;
submitHandler: (field: TitleActions, value: string) => void;
}
export default function CountedTextArea(props: CountedTextAreaProps) {
@@ -2,15 +2,16 @@ import { useCallback } from 'react';
import { Input } from '@chakra-ui/react';
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
import { EventEditorSubmitActions } from '../EventEditor';
import { TitleActions } from './EventEditorTitles';
import style from '../EventEditor.module.scss';
interface CountedTextInputProps {
field: EventEditorSubmitActions;
field: TitleActions;
label: string;
initialValue: string;
submitHandler: (field: EventEditorSubmitActions, value: string) => void;
submitHandler: (field: TitleActions, value: string) => void;
}
export default function CountedTextInput(props: CountedTextInputProps) {
@@ -0,0 +1,154 @@
import { memo, useState } from 'react';
import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToMinutes } from '../../../common/utils/dateConfig';
import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss';
interface EventEditorTimesProps {
eventId: string;
timeStart: number;
timeEnd: number;
duration: number;
delay: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
}
type TimeActions = 'timeStart' | 'timeEnd' | 'durationOverride' | 'timerType' | 'endAction' | 'isPublic';
// Todo: add previous end to TimeInput fields
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 };
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') {
// @ts-expect-error -- not sure how to typecheck here
newEventData[field as keyof OntimeEvent] = value as string;
} else {
return;
}
}
}
updateEvent(newEventData);
};
const delayed = delay !== 0;
const addedTime = delayed ? `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))} minutes` : null;
const newStart = delayed ? `New start ${millisToString(timeStart + delay)}` : null;
const newEnd = delayed ? `New end ${millisToString(timeEnd + delay)}` : null;
return (
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={style.inputLabel}>
Start time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newStart}</div>}
</label>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={timeStart}
delay={delay}
placeholder='Start'
warning={warning.start}
/>
<label className={style.inputLabel}>
End time {delayed && <span className={style.delayLabel}>{addedTime}</span>}
{delayed && <div className={style.delayLabel}>{newEnd}</div>}
</label>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={timeEnd}
delay={delay}
placeholder='End'
warning={warning.end}
/>
<label className={style.inputLabel}>Duration</label>
<TimeInput
name='durationOverride'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={duration}
placeholder='Duration'
warning={warning.duration}
/>
</div>
<div className={style.timeSettings}>
<label className={style.inputLabel}>Timer Type</label>
<Select
size='sm'
name='timerType'
value={timerType}
onChange={(event) => handleSubmit('timerType', event.target.value)}
variant='ontime'
>
<option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option>
<option value={TimerType.Clock}>Clock</option>
</Select>
<label className={style.inputLabel}>End Action</label>
<Select
size='sm'
name='endAction'
value={endAction}
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.Continue}>Continue</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
</Select>
<span className={style.spacer} />
<label className={`${style.inputLabel} ${style.publicToggle}`}>
<Switch isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
Event is public
</label>
</div>
</div>
);
};
export default memo(EventEditorTimes);
@@ -0,0 +1,50 @@
import { memo } from 'react';
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
import { useEventAction } from '../../../common/hooks/useEventAction';
import CountedTextArea from './CountedTextArea';
import CountedTextInput from './CountedTextInput';
import style from '../EventEditor.module.scss';
interface EventEditorTitlesProps {
eventId: string;
title: string;
presenter: string;
subtitle: string;
note: string;
colour: string;
}
export type TitleActions = 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
const EventEditorTitles = (props: EventEditorTitlesProps) => {
const { eventId, title, presenter, subtitle, note, colour } = props;
const { updateEvent } = useEventAction();
const handleSubmit = (field: TitleActions, value: string) => {
updateEvent({ id: eventId, [field]: value });
};
return (
<div className={style.titles}>
<div className={style.left}>
<CountedTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<CountedTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
<CountedTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
</div>
<div className={style.right}>
<div className={style.column}>
<label className={style.inputLabel}>Colour</label>
<div className={style.inline}>
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
</div>
</div>
<CountedTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
</div>
</div>
);
};
export default memo(EventEditorTitles);