mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 05:13:32 +00:00
feat: event cue (#473)
* feat: parse cue * refactor: get delay from backend * feat: add cue to UI * refactor: remove deprecated delay logic * refactor: extract studio clock specific logic * refactor: extract utilities * refactor: fix issue with missing key * style: prevent cue overflow * style: prevent whitespace wrap * feat: add support for cues in integrations
This commit is contained in:
@@ -6,10 +6,14 @@
|
||||
gap: max(1rem, 2vh);
|
||||
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'eventInfo eventActions'
|
||||
'timeOptions titles';
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-areas: 'time left right';
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
}
|
||||
|
||||
.timeOptions {
|
||||
grid-area: time;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
|
||||
.timers,
|
||||
.timeSettings {
|
||||
@@ -19,51 +23,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.eventInfo {
|
||||
grid-area: eventInfo;
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.eventId {
|
||||
margin-left:$element-spacing;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.eventActions {
|
||||
grid-area: eventActions;
|
||||
margin-left: auto;
|
||||
.left {
|
||||
grid-area: left;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
.timeOptions {
|
||||
grid-area: timeOptions;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.titles {
|
||||
grid-area: titles;
|
||||
display: grid;
|
||||
grid-template-areas: 'left right';
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.left {
|
||||
grid-area: left;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding-left: 1rem;
|
||||
grid-area: right;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
.right {
|
||||
padding-left: 1rem;
|
||||
grid-area: right;
|
||||
border-left: 1px solid $border-color-ondark;
|
||||
}
|
||||
|
||||
@mixin input-label() {
|
||||
@@ -94,6 +70,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.eventActions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 1.25rem;
|
||||
}
|
||||
@@ -104,6 +86,12 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.splitTwo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -116,4 +104,4 @@
|
||||
|
||||
.fullHeight {
|
||||
height: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import { useAppMode } from '../../common/stores/appModeStore';
|
||||
import getDelayTo from '../../common/utils/getDelayTo';
|
||||
|
||||
import EventEditorDataLeft from './composite/EventEditorDataLeft';
|
||||
import EventEditorDataRight from './composite/EventEditorDataRight';
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||
export type EditorUpdateFields = 'cue' | 'title' | 'presenter' | 'subtitle' | 'note' | 'colour';
|
||||
|
||||
export default function EventEditor() {
|
||||
const openId = useAppMode((state) => state.editId);
|
||||
const { data } = useRundown();
|
||||
const { updateEvent } = useEventAction();
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
const [delay, setDelay] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !openId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventIndex = data.findIndex((event) => event.id === openId);
|
||||
if (eventIndex > -1) {
|
||||
const event = data[eventIndex];
|
||||
if (event.type === 'event') {
|
||||
setDelay(getDelayTo(data, eventIndex));
|
||||
setEvent(data[eventIndex] as OntimeEvent);
|
||||
}
|
||||
const event = data.find((event) => event.id === openId);
|
||||
if (event && event.type === SupportedEvent.Event) {
|
||||
setEvent(event as OntimeEvent);
|
||||
}
|
||||
}, [data, event, openId]);
|
||||
}, [data, openId]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: EditorUpdateFields, value: string) => {
|
||||
updateEvent({ id: event?.id, [field]: value });
|
||||
},
|
||||
[event?.id, updateEvent],
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
return <span>Loading...</span>;
|
||||
@@ -40,34 +47,35 @@ export default function EventEditor() {
|
||||
|
||||
return (
|
||||
<div className={style.eventEditor}>
|
||||
<div className={style.eventInfo}>
|
||||
Event ID
|
||||
<span className={style.eventId}>
|
||||
<CopyTag label={event.id}>{event.id}</CopyTag>
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.eventActions}>
|
||||
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
</div>
|
||||
<EventEditorTimes
|
||||
eventId={event.id}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
duration={event.duration}
|
||||
delay={delay}
|
||||
delay={event.delay ?? 0}
|
||||
isPublic={event.isPublic}
|
||||
endAction={event.endAction}
|
||||
timerType={event.timerType}
|
||||
/>
|
||||
<EventEditorTitles
|
||||
key={event.id}
|
||||
<EventEditorDataLeft
|
||||
key={`${event.id}-left`}
|
||||
eventId={event.id}
|
||||
cue={event.cue}
|
||||
title={event.title}
|
||||
presenter={event.presenter}
|
||||
subtitle={event.subtitle}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<EventEditorDataRight
|
||||
key={`${event.id}-right`}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
/>
|
||||
handleSubmit={handleSubmit}
|
||||
>
|
||||
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
||||
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
||||
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue/${event.cue}`}</CopyTag>
|
||||
</EventEditorDataRight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Textarea } from '@chakra-ui/react';
|
||||
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
import { TitleActions } from './EventEditorTitles';
|
||||
import { TitleActions } from './EventEditorDataLeft';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
@@ -24,7 +24,9 @@ export default function CountedTextArea(props: CountedTextAreaProps) {
|
||||
return (
|
||||
<div className={`${style.column} ${style.fullHeight}`}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel} htmlFor={field}>
|
||||
{label}
|
||||
</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Textarea
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { Input, InputProps } from '@chakra-ui/react';
|
||||
import { sanitiseCue } from 'ontime-utils';
|
||||
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
|
||||
import { TitleActions } from './EventEditorTitles';
|
||||
import { EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface CountedTextInputProps {
|
||||
field: TitleActions;
|
||||
interface CountedTextInputProps extends InputProps {
|
||||
field: EditorUpdateFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
submitHandler: (field: TitleActions, value: string) => void;
|
||||
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
const { field, label, initialValue, submitHandler } = props;
|
||||
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
const submitCallback = useCallback(
|
||||
(newValue: string) => submitHandler(field, sanitiseCue(newValue)),
|
||||
[field, submitHandler],
|
||||
);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, {
|
||||
submitOnEnter: true,
|
||||
@@ -26,7 +29,9 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor={field}>{label}</label>
|
||||
<label className={style.inputLabel} htmlFor={field}>
|
||||
{label}
|
||||
</label>
|
||||
<span className={style.charCount}>{`${value.length} characters`}</span>
|
||||
</div>
|
||||
<Input
|
||||
@@ -35,6 +40,7 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textfield'
|
||||
value={value}
|
||||
maxLength={maxLength || 50}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { memo } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import { type EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import CountedTextInput from './CountedTextInput';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorLeftProps {
|
||||
eventId: string;
|
||||
cue: string;
|
||||
title: string;
|
||||
presenter: string;
|
||||
subtitle: string;
|
||||
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
const EventEditorDataLeft = (props: EventEditorLeftProps) => {
|
||||
const { eventId, cue, title, presenter, subtitle, handleSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className={style.left}>
|
||||
<div className={style.splitTwo}>
|
||||
<div className={style.column}>
|
||||
<div className={style.countedInput}>
|
||||
<label className={style.inputLabel} htmlFor='eventId'>
|
||||
Event ID (read only)
|
||||
</label>
|
||||
</div>
|
||||
<Input
|
||||
id='eventId'
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textfield'
|
||||
value={eventId}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<CountedTextInput field='cue' label='Cue' initialValue={cue} submitHandler={handleSubmit} maxLength={10} />
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventEditorDataLeft);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import { EditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import CountedTextArea from './CountedTextArea';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
|
||||
interface EventEditorRightProps {
|
||||
note: string;
|
||||
colour: string;
|
||||
handleSubmit: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
const EventEditorDataRight = (props: PropsWithChildren<EventEditorRightProps>) => {
|
||||
const { children, note, colour, handleSubmit } = props;
|
||||
|
||||
return (
|
||||
<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 className={style.eventActions}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventEditorDataRight);
|
||||
@@ -1,50 +0,0 @@
|
||||
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