mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
feat: edit groups
This commit is contained in:
committed by
Carlos Valente
parent
4c08482258
commit
473f50b493
@@ -0,0 +1,147 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeBlock } from 'ontime-types';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import NullableTimeInput from '../../../common/components/input/time-input/NullableTimeInput';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import Switch from '../../../common/components/switch/Switch';
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EntryEditorTextInput from './composite/EventTextInput';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
// title + colour + custom field labels
|
||||
export type BlockEditorUpdateTextFields = 'targetDuration' | 'title' | 'colour' | string;
|
||||
export type BlockEditorUpdateMaybeNumberFields = 'targetDuration';
|
||||
export type BlockEditorBooleanFields = 'isNextDay';
|
||||
|
||||
interface BlockEditorProps {
|
||||
block: OntimeBlock;
|
||||
}
|
||||
|
||||
export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(
|
||||
field: BlockEditorUpdateTextFields | BlockEditorUpdateMaybeNumberFields | BlockEditorBooleanFields,
|
||||
value: string | boolean,
|
||||
) => {
|
||||
// Handle custom fields
|
||||
if (typeof field === 'string' && field.startsWith('custom-')) {
|
||||
const fieldLabel = field.split('custom-')[1];
|
||||
updateEntry({ id: block.id, custom: { [fieldLabel]: value as string } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'targetDuration') {
|
||||
if (value === '') {
|
||||
return updateEntry({ id: block.id, targetDuration: null });
|
||||
}
|
||||
|
||||
return updateEntry({ id: block.id, targetDuration: parseUserTime(value as string) });
|
||||
}
|
||||
|
||||
if (field === 'isNextDay') {
|
||||
return updateEntry({ id: block.id, isNextDay: value as boolean });
|
||||
}
|
||||
|
||||
// all other strings are text fields
|
||||
return updateEntry({ id: block.id, [field]: value as string });
|
||||
},
|
||||
[block.id, updateEntry],
|
||||
);
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
const planOffset = typeof block.targetDuration !== 'number' ? null : block.targetDuration - block.duration;
|
||||
console.log('targetDuration:', block.targetDuration);
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Block schedule</Editor.Title>
|
||||
<div className={style.inline}>
|
||||
<div>
|
||||
{
|
||||
// TODO: format with user time settings
|
||||
}
|
||||
<Editor.Label>First event start</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.startTime, { fallback: timerPlaceholder })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='endTime'>Last event end</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.endTime, { fallback: timerPlaceholder })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='duration'>Scheduled duration</Editor.Label>
|
||||
<TextLikeInput className={style.textLikeInput}>
|
||||
{millisToString(block.duration, { fallback: enDash })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.inline}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='targetDuration'>Target duration</Editor.Label>
|
||||
<NullableTimeInput
|
||||
name='targetDuration'
|
||||
time={block.targetDuration}
|
||||
submitHandler={handleSubmit}
|
||||
emptyDisplay={enDash}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='eventId'>Plan offset</Editor.Label>
|
||||
{
|
||||
// TODO: update remote data
|
||||
// TODO: remove tab index
|
||||
}
|
||||
<TextLikeInput delayed={Boolean(planOffset)} className={style.textLikeInput}>
|
||||
{millisToString(planOffset, { fallback: enDash })}
|
||||
</TextLikeInput>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='isNextDay'>Is next day?</Editor.Label>
|
||||
<Editor.Label className={style.switchLabel}>
|
||||
<Switch
|
||||
checked={block.isNextDay}
|
||||
onCheckedChange={(checked) => {
|
||||
handleSubmit('isNextDay', checked);
|
||||
}}
|
||||
/>
|
||||
{block.isNextDay ? 'Events start the day after' : '-'}
|
||||
</Editor.Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Block data</Editor.Title>
|
||||
<div>
|
||||
<Editor.Label>Colour</Editor.Label>
|
||||
<SwatchSelect name='colour' value={block.colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={block.title} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} event={block} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import EventEditor from './EventEditor';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
interface CuesheetEventEditorProps {
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
export default function CuesheetEventEditor({ eventId }: CuesheetEventEditorProps) {
|
||||
const { data } = useRundown();
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = data.entries[eventId];
|
||||
if (event && isOntimeEvent(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [eventId, data.order, data.entries]);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx([style.entryEditor, style.inModal])} data-testid='editor-container'>
|
||||
<EventEditor event={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
.entryEditor {
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
|
||||
&.inModal {
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-inline: 0.5rem 1.5rem;
|
||||
padding-bottom: 4rem;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.timeSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: -0.5rem; // bring the title closer to the section elements
|
||||
}
|
||||
}
|
||||
|
||||
.decorated {
|
||||
color: var(--decorator-color, $ui-white);
|
||||
background-color: var(--decorator-bg, $gray-1100);
|
||||
width: fit-content;
|
||||
padding-inline: 0.5rem;
|
||||
border-radius: $component-border-radius-sm;
|
||||
}
|
||||
|
||||
.delayLabel {
|
||||
font-size: $aux-text-size;
|
||||
color: $ontime-delay-text;
|
||||
|
||||
&::after {
|
||||
content: '\200b';
|
||||
}
|
||||
}
|
||||
|
||||
.switchLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
max-width: max-content;
|
||||
cursor: pointer;
|
||||
height: 2rem; // manually match the height of a text input
|
||||
margin-bottom: 0; // reset margin from label component
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.splitTwo {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 1rem;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.tooltipIcon {
|
||||
color: $blue-500;
|
||||
display: inline-block;
|
||||
font-size: 1.25em;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.customImage {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 72px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* approximating the style of a disabled input */
|
||||
.textLikeInput {
|
||||
background-color: rgba($gray-1200, 0.4);
|
||||
font-weight: 400;
|
||||
color: $gray-200;
|
||||
border: 1px solid transparent;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
width: 7.5em;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($gray-1200, 0.4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
import EventEditorTriggers from './composite/EventEditorTriggers';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
// any of the titles + colour + custom field labels
|
||||
export type EventEditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | string;
|
||||
|
||||
interface EventEditorProps {
|
||||
event: OntimeEvent;
|
||||
}
|
||||
|
||||
export default function EventEditor({ event }: EventEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: EventEditorUpdateFields, value: string) => {
|
||||
if (field.startsWith('custom-')) {
|
||||
const fieldLabel = field.split('custom-')[1];
|
||||
updateEntry({ id: event.id, custom: { [fieldLabel]: value } });
|
||||
} else {
|
||||
updateEntry({ id: event.id, [field]: value });
|
||||
}
|
||||
},
|
||||
[event.id, updateEntry],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<EventEditorTimes
|
||||
key={`${event.id}-times`}
|
||||
eventId={event.id}
|
||||
timeStart={event.timeStart}
|
||||
timeEnd={event.timeEnd}
|
||||
duration={event.duration}
|
||||
timeStrategy={event.timeStrategy}
|
||||
linkStart={event.linkStart}
|
||||
countToEnd={event.countToEnd}
|
||||
delay={event.delay}
|
||||
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}
|
||||
note={event.note}
|
||||
colour={event.colour}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} event={event} />
|
||||
</div>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
Automations
|
||||
{isEditor && <AppLink search='settings=automation__automations'>Manage Automations</AppLink>}
|
||||
</Editor.Title>
|
||||
<EventEditorTriggers triggers={event.triggers} eventId={event.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.entryEditor {
|
||||
color: $label-gray;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.shortcutSection {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.shortcuts {
|
||||
font-size: calc(1rem - 3px);
|
||||
border-collapse: separate;
|
||||
border-spacing: 4rem 0;
|
||||
|
||||
tr {
|
||||
td:nth-child(odd) {
|
||||
text-align: left;
|
||||
}
|
||||
td:nth-child(even) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
margin-left: 4rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.kbd {
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
font-size: calc(1rem - 2px);
|
||||
padding: 0.125rem 0.5rem;
|
||||
background-color: $gray-1200;
|
||||
color: $ui-white;
|
||||
border-radius: 2px;
|
||||
font-weight: 400;
|
||||
box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
||||
|
||||
import style from './EventEditorEmpty.module.scss';
|
||||
|
||||
export default memo(EventEditorEmpty);
|
||||
|
||||
function EventEditorEmpty() {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<div className={style.shortcutSection}>
|
||||
<Editor.Title className={style.prompt}>Rundown shortcuts</Editor.Title>
|
||||
<table className={style.shortcuts}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Find in rundown</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>F</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Open Settings</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>,</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={style.spacer} />
|
||||
<tr>
|
||||
<td>Select entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>↓</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Select block</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>↓</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deselect entry</td>
|
||||
<td>
|
||||
<Kbd>Esc</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={style.spacer} />
|
||||
<tr>
|
||||
<td>Reorder selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>↑</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>↓</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Copy selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>C</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Paste above</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Paste below</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Backspace</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={style.spacer} />
|
||||
<tr>
|
||||
<td>Add event below</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>E</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add event above</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>E</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add block below</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>B</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add block above</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>B</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add delay below</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>D</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add delay above</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>D</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuxKey({ children }: PropsWithChildren) {
|
||||
return <span className={style.divider}>{children}</span>;
|
||||
}
|
||||
|
||||
function Kbd({ children }: PropsWithChildren) {
|
||||
return <span className={style.kbd}>{children}</span>;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeBlock, isOntimeDelay, OntimeBlock, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import EventEditorFooter from './composite/EventEditorFooter';
|
||||
import BlockEditor from './BlockEditor';
|
||||
import EventEditor from './EventEditor';
|
||||
import EventEditorEmpty from './EventEditorEmpty';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
export default function RundownEntryEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | OntimeBlock | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.order.length === 0) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = Array.from(selectedEvents).at(0);
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
}
|
||||
const event = data.entries[selectedEventId];
|
||||
|
||||
if (event && !isOntimeDelay(event)) {
|
||||
setEvent(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
}
|
||||
}, [data.order, data.entries, selectedEvents]);
|
||||
|
||||
if (!event) {
|
||||
return <EventEditorEmpty />;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(event)) {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<BlockEditor block={event} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<EventEditor event={event} />
|
||||
<EventEditorFooter id={event.id} cue={event.cue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { CSSProperties, Fragment } from 'react';
|
||||
import { CustomFields, OntimeBlock, OntimeEvent } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import EventEditorImage from './EventEditorImage';
|
||||
import EventTextArea from './EventTextArea';
|
||||
import EntryEditorTextInput from './EventTextInput';
|
||||
|
||||
import style from '../EntryEditor.module.scss';
|
||||
|
||||
interface EntryEditorCustomFieldsProps {
|
||||
fields: CustomFields;
|
||||
event: OntimeEvent | OntimeBlock;
|
||||
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function EntryEditorCustomFields({
|
||||
fields: customFields,
|
||||
handleSubmit,
|
||||
event,
|
||||
}: EntryEditorCustomFieldsProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
{Object.keys(customFields).map((fieldKey) => {
|
||||
const key = `${event.id}-${fieldKey}`;
|
||||
const fieldName = `custom-${fieldKey}`;
|
||||
const initialValue = event.custom[fieldKey] ?? '';
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
|
||||
const labelText = customFields[fieldKey].label;
|
||||
|
||||
if (customFields[fieldKey].type === 'string') {
|
||||
return (
|
||||
<EventTextArea
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (customFields[fieldKey].type === 'image') {
|
||||
return (
|
||||
<div key={key} className={style.customImage}>
|
||||
<EntryEditorTextInput
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
placeholder='Paste image URL'
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
maxLength={255}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
<EventEditorImage src={initialValue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// we should have exhausted all types by now
|
||||
return null;
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.footer {
|
||||
border-top: 1px solid $white-10;
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
|
||||
|
||||
import style from './EventEditorFooter.module.scss';
|
||||
|
||||
interface EventEditorFooterProps {
|
||||
id: string;
|
||||
cue: string;
|
||||
}
|
||||
|
||||
export default memo(EventEditorFooter);
|
||||
function EventEditorFooter({ id, cue }: EventEditorFooterProps) {
|
||||
const loadById = `/ontime/load/id "${id}"`;
|
||||
const loadByCue = `/ontime/load/cue "${cue}"`;
|
||||
|
||||
return (
|
||||
<div className={style.footer}>
|
||||
<CopyTag copyValue={loadById} label='OSC trigger by ID'>
|
||||
{loadById}
|
||||
</CopyTag>
|
||||
<CopyTag copyValue={loadByCue} label='OSC trigger by cue'>
|
||||
{loadByCue}
|
||||
</CopyTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.imageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $gray-1250;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.imageOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import style from './EventEditorImage.module.scss';
|
||||
|
||||
interface EventEditorImageProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export default function EventEditorImage({ src }: EventEditorImageProps) {
|
||||
return (
|
||||
<div className={style.imageContainer}>
|
||||
<img loading='lazy' src={src} />
|
||||
<div className={style.imageOverlay} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { memo } from 'react';
|
||||
import { IoInformationCircle } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Switch from '../../../../common/components/switch/Switch';
|
||||
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
||||
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||
import TimeInputFlow from '../../time-input-flow/TimeInputFlow';
|
||||
|
||||
import style from '../EntryEditor.module.scss';
|
||||
|
||||
interface EventEditorTimesProps {
|
||||
eventId: string;
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
timeStrategy: TimeStrategy;
|
||||
linkStart: boolean;
|
||||
countToEnd: boolean;
|
||||
delay: number;
|
||||
endAction: EndAction;
|
||||
timerType: TimerType;
|
||||
timeWarning: number;
|
||||
timeDanger: number;
|
||||
}
|
||||
|
||||
type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'timeWarning' | 'timeDanger';
|
||||
|
||||
export default memo(EventEditorTimes);
|
||||
function EventEditorTimes({
|
||||
eventId,
|
||||
timeStart,
|
||||
timeEnd,
|
||||
duration,
|
||||
timeStrategy,
|
||||
linkStart,
|
||||
countToEnd,
|
||||
delay,
|
||||
endAction,
|
||||
timerType,
|
||||
timeWarning,
|
||||
timeDanger,
|
||||
}: EventEditorTimesProps) {
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const handleSubmit = (field: HandledActions, value: string | boolean) => {
|
||||
if (field === 'countToEnd') {
|
||||
updateEntry({ id: eventId, countToEnd: value as boolean });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'timeWarning' || field === 'timeDanger') {
|
||||
const newTime = parseUserTime(value as string);
|
||||
updateEntry({ id: eventId, [field]: newTime });
|
||||
return;
|
||||
}
|
||||
|
||||
if (field === 'timerType' || field === 'endAction') {
|
||||
updateEntry({ id: eventId, [field]: value });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const hasDelay = delay !== 0;
|
||||
const delayLabel = hasDelay
|
||||
? `Event is ${millisToDelayString(delay, 'expanded')}. New schedule ${millisToString(
|
||||
timeStart + delay,
|
||||
)} → ${millisToString(timeEnd + delay)}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Event schedule</Editor.Title>
|
||||
<div>
|
||||
<div className={style.inline}>
|
||||
<TimeInputFlow
|
||||
eventId={eventId}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
timeStrategy={timeStrategy}
|
||||
linkStart={linkStart}
|
||||
delay={delay}
|
||||
countToEnd={countToEnd}
|
||||
showLabels
|
||||
/>
|
||||
</div>
|
||||
<div className={style.delayLabel}>{delayLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Event Behaviour</Editor.Title>
|
||||
<div className={style.splitTwo}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
|
||||
<Select
|
||||
value={endAction}
|
||||
onChange={(value) => handleSubmit('endAction', value)}
|
||||
options={[
|
||||
{ value: EndAction.None, label: 'None' },
|
||||
{ value: EndAction.LoadNext, label: 'Load next event' },
|
||||
{ value: EndAction.PlayNext, label: 'Play next event' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='countToEnd'>Count to End</Editor.Label>
|
||||
<Editor.Label className={style.switchLabel}>
|
||||
<Switch
|
||||
id='countToEnd'
|
||||
checked={countToEnd}
|
||||
onCheckedChange={(value) => handleSubmit('countToEnd', value)}
|
||||
/>
|
||||
{countToEnd ? 'On' : 'Off'}
|
||||
</Editor.Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
<Tooltip label='Changes how the timer is displayed in different views. It is not reflected in the rundown'>
|
||||
<span>
|
||||
Display Options
|
||||
<IoInformationCircle className={style.tooltipIcon} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Editor.Title>
|
||||
<div className={style.splitTwo}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
|
||||
<Select
|
||||
value={timerType}
|
||||
onChange={(value) => handleSubmit('timerType', value)}
|
||||
options={[
|
||||
{ value: TimerType.CountDown, label: 'Count down' },
|
||||
{ value: TimerType.CountUp, label: 'Count up' },
|
||||
{ value: TimerType.Clock, label: 'Clock' },
|
||||
{ value: TimerType.None, label: 'None' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.inline}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='timeWarning'>Warning Time</Editor.Label>
|
||||
<TimeInput
|
||||
id='timeWarning'
|
||||
name='timeWarning'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeWarning}
|
||||
placeholder='Duration'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label htmlFor='timeDanger'>Danger Time</Editor.Label>
|
||||
<TimeInput
|
||||
id='timeDanger'
|
||||
name='timeDanger'
|
||||
submitHandler={handleSubmit}
|
||||
time={timeDanger}
|
||||
placeholder='Duration'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { memo } from 'react';
|
||||
import { sanitiseCue } from 'ontime-utils';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
||||
import Input from '../../../../common/components/input/input/Input';
|
||||
import { type EventEditorUpdateFields } from '../EventEditor';
|
||||
|
||||
import EventTextArea from './EventTextArea';
|
||||
import EntryEditorTextInput from './EventTextInput';
|
||||
|
||||
import style from '../EntryEditor.module.scss';
|
||||
|
||||
interface EventEditorTitlesProps {
|
||||
eventId: string;
|
||||
cue: string;
|
||||
title: string;
|
||||
note: string;
|
||||
colour: string;
|
||||
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default memo(EventEditorTitles);
|
||||
function EventEditorTitles({ eventId, cue, title, note, colour, handleSubmit }: EventEditorTitlesProps) {
|
||||
const cueSubmitHandler = (_field: string, newValue: string) => {
|
||||
handleSubmit('cue', sanitiseCue(newValue));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Event Data</Editor.Title>
|
||||
<div className={style.splitTwo}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label>
|
||||
<Input id='eventId' data-testid='input-textfield' value={eventId} readOnly fluid />
|
||||
</div>
|
||||
<EntryEditorTextInput
|
||||
field='cue'
|
||||
label='Cue'
|
||||
initialValue={cue}
|
||||
submitHandler={cueSubmitHandler}
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label>Colour</Editor.Label>
|
||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.triggerForm {
|
||||
padding-block: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto auto;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: $white-1;
|
||||
}
|
||||
|
||||
& > span {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.errorLabel {
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: $green-500;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { TimerLifeCycle, timerLifecycleValues, Trigger } from 'ontime-types';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
|
||||
import { eventTriggerOptions } from './eventTrigger.constants';
|
||||
|
||||
import style from './EventEditorTriggers.module.scss';
|
||||
|
||||
interface EventEditorTriggersProps {
|
||||
eventId: string;
|
||||
triggers: Trigger[];
|
||||
}
|
||||
|
||||
export default function EventEditorTriggers({ triggers, eventId }: EventEditorTriggersProps) {
|
||||
const showTriggers = triggers.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTriggers && <ExistingEventTriggers triggers={triggers} eventId={eventId} />}
|
||||
<EventTriggerForm triggers={triggers} eventId={eventId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventTriggerFormProps {
|
||||
eventId: string;
|
||||
triggers?: Trigger[];
|
||||
}
|
||||
|
||||
function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
||||
const { data: automationSettings } = useAutomationSettings();
|
||||
const { updateEntry } = useEntryActions();
|
||||
const [automationId, setAutomationId] = useState<string | undefined>(undefined);
|
||||
const [cycleValue, setCycleValue] = useState(TimerLifeCycle.onStart);
|
||||
|
||||
const handleSubmit = (triggerLifeCycle: TimerLifeCycle, automationId: string) => {
|
||||
const newTriggers = triggers ?? new Array<Trigger>();
|
||||
const id = generateId();
|
||||
newTriggers.push({ id, title: '', trigger: triggerLifeCycle, automationId });
|
||||
updateEntry({ id: eventId, triggers: newTriggers });
|
||||
};
|
||||
|
||||
const getValidationError = (cycle: TimerLifeCycle, automationId?: string): string | undefined => {
|
||||
if (automationId === undefined) {
|
||||
return 'Select an automation';
|
||||
}
|
||||
if (!Object.keys(automationSettings.automations).includes(automationId)) {
|
||||
return 'This automation does not exist';
|
||||
}
|
||||
if (triggers === undefined) {
|
||||
return;
|
||||
}
|
||||
return Object.values(triggers).some((t) => t.automationId === automationId && t.trigger === cycle)
|
||||
? 'Automation can only be used once'
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const validationError = getValidationError(cycleValue, automationId);
|
||||
|
||||
return (
|
||||
<div className={style.triggerForm}>
|
||||
<Select
|
||||
value={cycleValue}
|
||||
placeholder='Choose a trigger'
|
||||
onChange={(value) => setCycleValue(value)}
|
||||
options={eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle }))}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={automationId}
|
||||
placeholder='Choose an automation'
|
||||
onChange={(value) => setAutomationId(value)}
|
||||
options={Object.values(automationSettings.automations).map(({ id, title }) => ({ value: id, label: title }))}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={validationError !== undefined}
|
||||
onClick={() => automationId && handleSubmit(cycleValue, automationId)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
{validationError !== undefined ? (
|
||||
<Tooltip label={validationError} shouldWrapChildren>
|
||||
<IoAlertCircle className={style.errorLabel} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<IoCheckmarkCircle className={style.success} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExistingEventTriggersProps {
|
||||
eventId: string;
|
||||
triggers: Trigger[];
|
||||
}
|
||||
|
||||
function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps) {
|
||||
const { updateEntry } = useEntryActions();
|
||||
const { data: automationSettings } = useAutomationSettings();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(triggerId: string) => {
|
||||
const newTriggers = triggers.filter((trigger) => trigger.id !== triggerId);
|
||||
updateEntry({ id: eventId, triggers: newTriggers });
|
||||
},
|
||||
[eventId, triggers, updateEntry],
|
||||
);
|
||||
|
||||
const filteredTriggers: Record<string, Trigger[]> = {};
|
||||
|
||||
// sort triggers out into groups by the Lifecycle they are on
|
||||
timerLifecycleValues.forEach((triggerType) => {
|
||||
const thisTriggerType = triggers.filter((trigger) => trigger.trigger === triggerType);
|
||||
if (thisTriggerType.length) {
|
||||
Object.assign(filteredTriggers, { [triggerType]: thisTriggerType });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.entries(filteredTriggers).map(([triggerLifeCycle, triggerGroup]) => (
|
||||
<Fragment key={triggerLifeCycle}>
|
||||
{triggerGroup.map((trigger) => {
|
||||
const { id, automationId } = trigger;
|
||||
const automationTitle = automationSettings.automations[automationId]?.title ?? '<MISSING AUTOMATION>';
|
||||
return (
|
||||
<div key={id} className={style.trigger}>
|
||||
<Tag>{triggerLifeCycle}</Tag>
|
||||
<Tag>{automationTitle}</Tag>
|
||||
<IconButton variant='subtle-destructive' onClick={() => handleDelete(id)}>
|
||||
<IoTrash />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type CSSProperties, useCallback, useRef } from 'react';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
|
||||
interface CountedTextAreaProps {
|
||||
className?: string;
|
||||
field: EventEditorUpdateFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
style?: CSSProperties;
|
||||
submitHandler: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function EventTextArea({
|
||||
className,
|
||||
field,
|
||||
label,
|
||||
initialValue,
|
||||
style: givenStyles,
|
||||
submitHandler,
|
||||
}: CountedTextAreaProps) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
submitOnCtrlEnter: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
||||
{label}
|
||||
</Editor.Label>
|
||||
<AutoTextArea
|
||||
id={field}
|
||||
inputref={ref}
|
||||
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,53 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import Input, { type InputProps } from '../../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { BlockEditorUpdateTextFields } from '../BlockEditor';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
|
||||
interface EntryEditorTextInputProps extends InputProps {
|
||||
field: EventEditorUpdateFields | BlockEditorUpdateTextFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
placeholder?: string;
|
||||
submitHandler: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function EntryEditorTextInput({
|
||||
className,
|
||||
field,
|
||||
label,
|
||||
initialValue,
|
||||
style: givenStyles,
|
||||
submitHandler,
|
||||
maxLength,
|
||||
placeholder,
|
||||
}: EntryEditorTextInputProps) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
submitOnEnter: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
||||
{label}
|
||||
</Editor.Label>
|
||||
<Input
|
||||
id={field}
|
||||
ref={ref}
|
||||
maxLength={maxLength}
|
||||
fluid
|
||||
data-testid='input-textfield'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
export const eventTriggerOptions: TimerLifeCycle[] = [
|
||||
TimerLifeCycle.onLoad,
|
||||
TimerLifeCycle.onStart,
|
||||
TimerLifeCycle.onPause,
|
||||
TimerLifeCycle.onFinish,
|
||||
TimerLifeCycle.onWarning,
|
||||
TimerLifeCycle.onDanger,
|
||||
];
|
||||
Reference in New Issue
Block a user