mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 09:23:51 +00:00
refactor: performance tweaks to event editor
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user