Event editor continue (#722)

* refactor: remove info panel

* feat: editor panel

* wip: event info in drawer

* chore: update tests

* chore: add scss to processor
This commit is contained in:
Carlos Valente
2024-01-22 16:23:11 +01:00
committed by GitHub
parent c7328a33f6
commit cb8ba776e2
86 changed files with 686 additions and 773 deletions
@@ -1,8 +1,11 @@
@use '../../theme/v2Styles' as *;
.rundownWrapper {
height: calc(100% - 1.5rem);
overflow: hidden;
padding-top: 0.5rem;
flex: 1;
}
.eventContainer {
flex: 1;
margin-top: 1em;
display: flex;
flex-direction: column;
+2 -4
View File
@@ -33,8 +33,6 @@ export default function Rundown(props: RundownProps) {
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
const showQuickEntry = eventSettings.showQuickEntry;
const isExtracted = window.location.pathname.includes('/rundown');
// cursor
const { cursor, mode: appMode } = useAppMode();
const viewFollowsCursor = appMode === AppMode.Run;
@@ -206,7 +204,7 @@ export default function Rundown(props: RundownProps) {
let isPast = Boolean(featureData?.selectedEventId);
return (
<div className={style.eventContainer} ref={scrollRef}>
<div className={style.eventContainer} ref={scrollRef} data-testid='rundown'>
<DndContext onDragEnd={handleOnDragEnd} sensors={sensors} collisionDetection={closestCenter}>
<SortableContext items={statefulEntries} strategy={verticalListSortingStrategy}>
<div className={style.list}>
@@ -248,7 +246,7 @@ export default function Rundown(props: RundownProps) {
previousEventId={previousEventId}
playback={isSelected ? featureData.playback : undefined}
isRolling={featureData.playback === Playback.Roll}
disableEdit={isExtracted || appMode === AppMode.Run}
disableEdit={appMode === AppMode.Run}
/>
</div>
</div>
@@ -4,17 +4,25 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import { handleLinks } from '../../common/utils/linkUtils';
import EventEditor from './event-editor/EventEditor';
import RundownWrapper from './RundownWrapper';
import style from '../editors/Editor.module.scss';
import style from './RundownWrapper.module.scss';
const RundownExport = () => {
const isExtracted = window.location.pathname.includes('/rundown');
return (
<div className={style.rundown} data-testid='panel-rundown'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'rundown')} />
<ErrorBoundary>
<RundownWrapper />
</ErrorBoundary>
<div className={style.rundownExport} data-testid='panel-rundown'>
{!isExtracted && <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'rundown')} />}
<div className={style.rundown}>
<ErrorBoundary>
<RundownWrapper />
</ErrorBoundary>
<ErrorBoundary>
<EventEditor />
</ErrorBoundary>
</div>
</div>
);
};
@@ -0,0 +1,21 @@
@use '../editors//EditorMixin' as editor;
$rundown-width: 44.5rem;
$editor-width: 25rem;
.rundownExport {
grid-area: rundown;
min-width: calc($rundown-width + $editor-width);
max-height: 100%;
position: relative;
}
.rundown {
@include editor.panel;
}
.corner {
@include editor.corner;
display: inline;
z-index: 2;
}
@@ -4,17 +4,17 @@ import RundownMenu from '../menu/RundownMenu';
import Rundown from './Rundown';
import styles from '../editors/Editor.module.scss';
import styles from './Rundown.module.scss';
export default function RundownWrapper() {
const { data, status } = useRundown();
return (
<>
<div>
<RundownMenu />
<div className={styles.content}>
<div className={styles.rundownWrapper}>
{status === 'success' && data ? <Rundown entries={data} /> : <Empty text='Connecting to server' />}
</div>
</>
</div>
);
}
@@ -1,5 +1,5 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
@use '../../theme/ontimeStyles' as *;
$block-gap: 0.25rem;
$block-element-spacing: 0.25rem;
@@ -1,5 +1,3 @@
@use '../../../theme/v2Styles' as *;
.titleEditor {
display: block;
font-size: calc(1rem + 2px);
@@ -1,5 +1,3 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
@use '../blockMixins' as *;
$skip-opacity: 0.1;
@@ -1,5 +1,3 @@
@use '../../../../theme/v2Styles' as *;
.progressBar {
height: 100%;
width: 0;
@@ -0,0 +1,84 @@
// TODO: unify with RundownWrapper.module.scss
$editor-width: 25rem;
.eventEditor {
min-width: $editor-width;
background-color: $gray-1350;
max-height: 100%;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.header {
padding: 1rem;
}
.content {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 2rem;
overflow-y: auto;
}
.footer {
padding: 1rem;
}
.timeSettings {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.column {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.inputLabel {
display: block;
font-size: calc(1rem - 3px);
color: $label-gray;
margin-bottom: 0.25rem;
&.delayLabel {
color: $ontime-delay-text;
}
}
.switchLabel {
font-size: calc(1rem - 3px);
color: $label-gray;
display: flex;
align-items: center;
gap: 0.5rem;
max-width: max-content;
cursor: pointer;
}
.inline {
display: flex;
align-items: center;
gap: 1rem;
}
.splitTwo {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1.5rem;
row-gap: 0.5rem;
}
.eventActions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
@@ -0,0 +1,118 @@
import { useCallback, useEffect, useState } from 'react';
import { isOntimeEvent, OntimeEvent } 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 { useEventSelection } from '../useEventSelection';
import EventEditorTimes from './composite/EventEditorTimes';
import EventEditorTitles from './composite/EventEditorTitles';
import EventEditorUser from './composite/EventEditorUser';
import style from './EventEditor.module.scss';
export type EventEditorSubmitActions = keyof OntimeEvent;
// TODO: this logic will become dynamic
export type EditorUpdateFields =
| 'cue'
| 'title'
| 'presenter'
| 'subtitle'
| 'note'
| 'colour'
| 'user0'
| 'user1'
| 'user2'
| 'user3'
| 'user4'
| 'user5'
| 'user6'
| 'user7'
| 'user8'
| 'user9';
export default function EventEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
const { updateEvent } = useEventAction();
const [event, setEvent] = useState<OntimeEvent | null>(null);
useEffect(() => {
if (!data) {
setEvent(null);
return;
}
const event = data.find((event) => selectedEvents.has(event.id));
if (event && isOntimeEvent(event)) {
setEvent(event);
}
}, [data, selectedEvents]);
const handleSubmit = useCallback(
(field: EditorUpdateFields, value: string) => {
updateEvent({ id: event?.id, [field]: value });
},
[event?.id, updateEvent],
);
if (!event) {
return <span>Loading...</span>;
}
// Compositing user fields by hand
// this will be replaced by dynamic logic
const userFields = {
user0: event.user0,
user1: event.user1,
user2: event.user2,
user3: event.user3,
user4: event.user4,
user5: event.user5,
user6: event.user6,
user7: event.user7,
user8: event.user8,
user9: event.user9,
};
return (
<div className={style.eventEditor}>
<div>HEADER ACTIONS?</div>
<div className={style.content}>
<EventEditorTimes
key={`${event.id}-times`}
eventId={event.id}
timeStart={event.timeStart}
timeEnd={event.timeEnd}
duration={event.duration}
delay={event.delay ?? 0}
isPublic={event.isPublic}
endAction={event.endAction}
timerType={event.timerType}
timeWarning={event.timeWarning}
timeDanger={event.timeDanger}
/>
<EventEditorTitles
key={`${event.id}-titles`}
eventId={event.id}
cue={event.cue}
title={event.title}
presenter={event.presenter}
subtitle={event.subtitle}
note={event.note}
colour={event.colour}
handleSubmit={handleSubmit}
/>
<EventEditorUser key={`${event.id}-user`} userFields={userFields} handleSubmit={handleSubmit} />
</div>
<div className={style.eventActions}>
<CopyTag label='OSC trigger by id'>{`/ontime/load/id "${event.id}"`}</CopyTag>
<CopyTag label='OSC trigger by cue'>{`/ontime/load/cue "${event.cue}"`}</CopyTag>
</div>
</div>
);
}
@@ -0,0 +1,189 @@
import { memo } from 'react';
import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import TimeInputWithButton from '../../../../common/components/input/time-input/TimeInputWithButton';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
import { cx } from '../../../../common/utils/styleUtils';
import style from '../EventEditor.module.scss';
interface EventEditorTimesProps {
eventId: string;
timeStart: number;
timeEnd: number;
duration: number;
delay: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
timeWarning: number;
timeDanger: number;
}
type TimeActions =
| 'timeStart'
| 'timeEnd'
| 'durationOverride'
| 'timerType'
| 'endAction'
| 'isPublic'
| 'timeWarning'
| 'timeDanger';
const EventEditorTimes = (props: EventEditorTimesProps) => {
const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType, timeWarning, timeDanger } =
props;
const { updateEvent } = useEventAction();
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' || field === 'timeWarning' || field === 'timeDanger') {
// @ts-expect-error -- not sure how to typecheck here
newEventData[field as keyof OntimeEvent] = value as string;
} else {
return;
}
}
}
updateEvent(newEventData);
};
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
const startLabel = delayTime ? `New start ${millisToString(timeStart + delay)}` : 'Start time';
const endLabel = delayTime ? `New end ${millisToString(timeEnd + delay)}` : 'End time';
const inputTimeLabels = cx([style.inputLabel, delayTime ? style.delayLabel : null]);
return (
<div className={style.column}>
<div className={style.inline}>
<div>
<label className={inputTimeLabels} htmlFor='timeStart'>
{startLabel}
</label>
<TimeInputWithButton
id='timeStart'
name='timeStart'
submitHandler={handleSubmit}
time={timeStart}
delay={delay}
placeholder='Start'
/>
</div>
<div>
<label className={inputTimeLabels} htmlFor='timeEnd'>
{endLabel}
</label>
<TimeInputWithButton
id='timeEnd'
name='timeEnd'
submitHandler={handleSubmit}
time={timeEnd}
delay={delay}
placeholder='End'
/>
</div>
<div>
<label className={style.inputLabel} htmlFor='durationOverride'>
Duration
</label>
<TimeInputWithButton
id='durationOverride'
name='durationOverride'
submitHandler={handleSubmit}
time={duration}
placeholder='Duration'
/>
</div>
</div>
<div className={style.splitTwo}>
<label className={style.inputLabel} htmlFor='timeWarning'>
Warning Time
<TimeInput
id='timeWarning'
name='timeWarning'
submitHandler={handleSubmit}
time={timeWarning}
placeholder='Duration'
/>
</label>
<label className={style.inputLabel}>
Timer Type
<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.TimeToEnd}>Time to end</option>
<option value={TimerType.Clock}>Clock</option>
</Select>
</label>
<label className={style.inputLabel} htmlFor='timeDanger'>
Danger Time
<TimeInput
id='timeDanger'
name='timeDanger'
submitHandler={handleSubmit}
time={timeDanger}
placeholder='Duration'
/>
</label>
<label className={style.inputLabel}>
End Action
<Select
size='sm'
name='endAction'
value={endAction}
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.None}>None</option>
<option value={EndAction.Stop}>Stop</option>
<option value={EndAction.LoadNext}>Load Next</option>
<option value={EndAction.PlayNext}>Play Next</option>
</Select>
</label>
</div>
<div>
<span className={style.inputLabel}>Event visibility</span>
<label className={style.switchLabel}>
<Switch isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
{isPublic ? 'Public' : 'Private'}
</label>
</div>
</div>
);
};
export default memo(EventEditorTimes);
@@ -0,0 +1,59 @@
import { memo } from 'react';
import { Input } from '@chakra-ui/react';
import { sanitiseCue } from 'ontime-utils';
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
import { type EditorUpdateFields } from '../EventEditor';
import EventTextArea from './EventTextArea';
import EventTextInput from './EventTextInput';
import style from '../EventEditor.module.scss';
interface EventEditorLeftProps {
eventId: string;
cue: string;
title: string;
presenter: string;
subtitle: string;
note: string;
colour: string;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
}
const EventEditorTitles = (props: EventEditorLeftProps) => {
const { eventId, cue, title, presenter, subtitle, note, colour, handleSubmit } = props;
const cueSubmitHandler = (_field: string, newValue: string) => {
handleSubmit('cue', sanitiseCue(newValue));
};
return (
<div className={style.column}>
<div className={style.splitTwo}>
<label className={style.inputLabel} htmlFor='eventId'>
Event ID (read only)
<Input
id='eventId'
size='sm'
variant='ontime-filled'
data-testid='input-textfield'
value={eventId}
readOnly
/>
</label>
<EventTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
</div>
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<EventTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
<EventTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
<div>
<label className={style.inputLabel}>Colour</label>
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
</div>
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
</div>
);
};
export default memo(EventEditorTitles);
@@ -0,0 +1,83 @@
import { UserFields } from 'ontime-types';
import useUserFields from '../../../../common/hooks-query/useUserFields';
import { EditorUpdateFields } from '../EventEditor';
import EventTextArea from './EventTextArea';
import style from '../EventEditor.module.scss';
interface EventEditorUserProps {
userFields: UserFields;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
}
export default function EventEditorUser(props: EventEditorUserProps) {
const { userFields, handleSubmit } = props;
const { data } = useUserFields();
return (
<div className={style.column}>
<EventTextArea
field='user0'
label={data?.user0 ?? 'user0'}
initialValue={userFields.user0}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user1'
label={data?.user1 ?? 'user1'}
initialValue={userFields.user1}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user2'
label={data?.user2 ?? 'user2'}
initialValue={userFields.user2}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user3'
label={data?.user3 ?? 'user3'}
initialValue={userFields.user3}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user4'
label={data?.user4 ?? 'user4'}
initialValue={userFields.user4}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user5'
label={data?.user5 ?? 'user5'}
initialValue={userFields.user5}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user6'
label={data?.user6 ?? 'user6'}
initialValue={userFields.user6}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user7'
label={data?.user7 ?? 'user7'}
initialValue={userFields.user7}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user8'
label={data?.user8 ?? 'user8'}
initialValue={userFields.user8}
submitHandler={handleSubmit}
/>
<EventTextArea
field='user9'
label={data?.user9 ?? 'user9'}
initialValue={userFields.user9}
submitHandler={handleSubmit}
/>
</div>
);
}
@@ -0,0 +1,42 @@
import { useCallback } from 'react';
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
import { EditorUpdateFields } from '../EventEditor';
import style from '../EventEditor.module.scss';
interface CountedTextAreaProps {
field: EditorUpdateFields;
label: string;
initialValue: string;
submitHandler: (field: EditorUpdateFields, value: string) => void;
}
export default function EventTextArea(props: CountedTextAreaProps) {
const { field, label, initialValue, submitHandler } = props;
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback);
return (
<div>
<label className={style.inputLabel} htmlFor={field}>
{label}
</label>
<AutoTextArea
id={field}
rows={1}
size='sm'
resize='none'
variant='ontime-filled'
data-testid='input-textarea'
value={value}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
</div>
);
}
@@ -0,0 +1,44 @@
import { useCallback } from 'react';
import { Input, InputProps } from '@chakra-ui/react';
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
import { EditorUpdateFields } from '../EventEditor';
import style from '../EventEditor.module.scss';
interface CountedTextInputProps extends InputProps {
field: EditorUpdateFields;
label: string;
initialValue: string;
submitHandler: (field: EditorUpdateFields, value: string) => void;
}
export default function EventTextInput(props: CountedTextInputProps) {
const { field, label, initialValue, submitHandler, maxLength } = props;
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, {
submitOnEnter: true,
});
return (
<div>
<label className={style.inputLabel} htmlFor={field}>
{label}
</label>
<Input
id={field}
size='sm'
variant='ontime-filled'
data-testid='input-textfield'
value={value}
maxLength={maxLength || 50}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
autoComplete='off'
/>
</div>
);
}
@@ -1,5 +1,3 @@
@use '../../../theme/v2Styles' as *;
.quickAdd {
display: grid;
grid-template-columns: 1fr auto;