feat: edit groups

This commit is contained in:
Carlos Valente
2025-06-25 06:10:12 +02:00
committed by Carlos Valente
parent 4c08482258
commit 473f50b493
75 changed files with 916 additions and 511 deletions
@@ -9,12 +9,13 @@
border: 1px solid transparent;
padding-inline: 0.5em;
outline: none;
&:hover:not(:disabled) {
background-color: $gray-1100;
}
&:focus {
&:focus:not(:read-only) {
background-color: $gray-1000;
border: 1px solid $blue-500;
}
@@ -26,6 +27,7 @@
&::placeholder {
color: $gray-500;
letter-spacing: 0;
}
}
@@ -36,3 +38,7 @@
.large {
height: 2.5rem;
}
.fluid {
width: 100%;
}
@@ -4,20 +4,23 @@ import { cx } from '../../../utils/styleUtils';
import style from './Input.module.scss';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
variant?: 'subtle';
height?: 'medium' | 'large';
fluid?: boolean;
}
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ className, variant = 'subtle', height = 'medium', ...inputProps },
{ className, variant = 'subtle', height = 'medium', fluid, ...inputProps },
ref,
) {
return (
<input
ref={ref}
type='text'
className={cx([style.input, style[variant], style[height], className])}
autoCorrect='off'
autoComplete='off'
className={cx([style.input, style[variant], style[height], fluid && style.fluid, className])}
{...inputProps}
/>
);
@@ -0,0 +1,135 @@
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
import { millisToString, parseUserTime } from 'ontime-utils';
import { cx } from '../../../utils/styleUtils';
import Input from '../input/Input';
import style from './TimeInput.module.scss';
interface NullableTimeInputProps<T extends string> {
id?: T;
name: T;
submitHandler: (field: T, value: string) => void;
time?: number | null;
emptyDisplay: string;
placeholder?: string;
disabled?: boolean;
align?: 'left' | 'center';
className?: string;
}
export default function NullableTimeInput<T extends string>(props: NullableTimeInputProps<T>) {
const { id, name, submitHandler, time, emptyDisplay, placeholder, disabled, align = 'center', className } = props;
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
const ignoreChange = useRef(false);
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
if (typeof time !== 'number' || isNaN(time)) {
setValue(emptyDisplay);
} else {
setValue(millisToString(time));
}
}, [emptyDisplay, time]);
/**
* @description Selects input text on focus
*/
const handleFocus = useCallback(() => {
inputRef.current?.select();
}, []);
/**
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback(
(newValue: string) => {
// skip if user deleted and time is already null
if (newValue === '' && time === null) {
return false;
}
// skip if the value evaluates to the same thing
const valueInMillis = parseUserTime(newValue);
if (valueInMillis === time) {
return false;
}
submitHandler(name, newValue);
return true;
},
[name, submitHandler, time],
);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback(
(newValue: string) => {
const success = handleSubmit(newValue);
if (!success) {
resetValue();
}
},
[handleSubmit, resetValue],
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
inputRef.current?.blur();
}
if (event.key === 'Escape') {
ignoreChange.current = true;
inputRef.current?.blur();
resetValue();
}
},
[resetValue],
);
const onBlurHandler = useCallback(
(event: FocusEvent<HTMLInputElement>) => {
if (ignoreChange.current) {
ignoreChange.current = false;
return;
}
validateAndSubmit((event.target as HTMLInputElement).value);
},
[validateAndSubmit],
);
useEffect(() => {
resetValue();
}, [resetValue]);
return (
<Input
id={id}
disabled={disabled}
ref={inputRef}
data-testid={`time-input-${name}`}
className={cx([style.timeInput, className])}
placeholder={placeholder}
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={onBlurHandler}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
autoComplete='off'
style={{
textAlign: align,
}}
/>
);
}
@@ -2,6 +2,5 @@
width: 100%;
max-width: 7.5em;
letter-spacing: 1px;
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
@@ -1,7 +1,6 @@
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
import { millisToString, parseUserTime } from 'ontime-utils';
import { useEmitLog } from '../../../stores/logger';
import { cx } from '../../../utils/styleUtils';
import Input from '../input/Input';
@@ -19,8 +18,7 @@ interface TimeInputProps<T extends string> {
}
export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
const { id, name, submitHandler, time = 0, placeholder, disabled, align = 'center', className } = props;
const { emitError } = useEmitLog();
const { id, name, submitHandler, time, placeholder, disabled, align = 'center', className } = props;
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
const ignoreChange = useRef(false);
@@ -29,16 +27,12 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
try {
if (typeof time !== 'number' || isNaN(time)) {
throw new Error(`Invalid time value: ${time}`);
}
if (typeof time !== 'number' || isNaN(time)) {
setValue('00:00:00');
} else {
setValue(millisToString(time));
} catch (error) {
setValue(millisToString(0));
emitError(`Unable to parse time ${time}: ${error}`);
}
}, [emitError, time]);
}, [time]);
/**
* @description Selects input text on focus
@@ -119,9 +113,8 @@ export default function TimeInput<T extends string>(props: TimeInputProps<T>) {
);
useEffect(() => {
if (time == null) return;
resetValue();
}, [resetValue, time]);
}, [resetValue]);
return (
<Input
@@ -92,17 +92,15 @@
}
.scrollArrow {
background-color: $gray-1000;
color: $ui-white;
width: 100%;
background: canvas;
z-index: 1;
text-align: center;
cursor: default;
border-radius: 0.375rem;
height: 1rem;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.5rem;
display: grid;
place-content: center;
&::before {
content: '';
@@ -113,6 +111,7 @@
}
&[data-direction='up'] {
border-radius: 3px 3px 0 0;
&::before {
top: -100%;
}
@@ -120,6 +119,7 @@
&[data-direction='down'] {
bottom: 0;
border-radius: 0 0 3px 3px;
&::before {
bottom: -100%;
@@ -9,6 +9,7 @@ interface SelectProps<T extends string | null = string> {
options: {
value: NonNullable<T>;
label: string;
disabled?: boolean; // exposed to allow creating a non-selectable option
}[];
placeholder?: string;
value?: T;
@@ -0,0 +1,52 @@
.switch {
position: relative;
display: flex;
align-items: center;
/* Reset and base styles */
appearance: none;
border: 1px solid transparent;
border-radius: 99px;
background-color: $gray-1100;
/* Transitions */
transition: background-color 125ms cubic-bezier(0.26, 0.75, 0.38, 0.45);
/* States */
&[data-checked] {
background-color: $blue-700;
box-shadow: none;
}
&:focus {
border-color: $blue-500;
}
}
.medium {
padding: 2px;
--width: calc(2.5rem + 4px);
--height: 1.5rem;
width: var(--width);
height: var(--height);
}
.large {
padding: 3px;
--width: calc(2.75rem + 6px);
--height: 1.75rem;
width: var(--width);
height: var(--height);
}
.thumb {
aspect-ratio: 1 / 1;
height: 100%;
border-radius: 99px;
background-color: $ui-white;
transition: translate 150ms ease;
&[data-checked] {
translate: calc(var(--width) - var(--height)) 0;
}
}
@@ -0,0 +1,17 @@
import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
import { cx } from '../../utils/styleUtils';
import style from './Switch.module.scss';
interface SwitchProps extends BaseSwitch.Root.Props {
size?: 'medium' | 'large';
}
export default function Switch({ size = 'medium', ...switchProps }: SwitchProps) {
return (
<BaseSwitch.Root className={cx([style.switch, style[size]])} {...switchProps}>
<BaseSwitch.Thumb className={style.thumb} />
</BaseSwitch.Root>
);
}
@@ -790,7 +790,7 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) {
order = order.filter((id) => id !== entry.id);
} else {
const parent = entries[entry.parent] as OntimeBlock;
parent.events = parent.events.filter((event) => event !== entry.id);
parent.entries = parent.entries.filter((parentEntry) => parentEntry !== entry.id);
}
delete entries[entry.id];
+14 -14
View File
@@ -171,21 +171,21 @@ export default function Operator() {
return (
<Fragment key={entry.id}>
<OperatorBlock key={entry.id} title={entry.title} />
{entry.events.map((nestedEventId) => {
const nestedEvent = data.entries[nestedEventId];
if (!isOntimeEvent(nestedEvent)) {
{entry.entries.map((nestedEntryId) => {
const nestedEntry = data.entries[nestedEntryId];
if (!isOntimeEvent(nestedEntry)) {
return null;
}
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEvent);
const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry);
// hide past events (if setting) and skipped events
if ((hidePast && isPast) || entry.skip) {
if (hidePast && isPast) {
return null;
}
const { mainField, secondaryField, subscribedData } = getEventData(
nestedEvent,
nestedEntry,
mainSource,
secondarySource,
subscribe,
@@ -194,16 +194,16 @@ export default function Operator() {
return (
<OperatorEvent
key={nestedEvent.id}
id={nestedEvent.id}
colour={nestedEvent.colour}
cue={nestedEvent.cue}
key={nestedEntry.id}
id={nestedEntry.id}
colour={nestedEntry.colour}
cue={nestedEntry.cue}
main={mainField}
secondary={secondaryField}
timeStart={nestedEvent.timeStart}
duration={nestedEvent.duration}
delay={nestedEvent.delay}
dayOffset={nestedEvent.dayOffset}
timeStart={nestedEntry.timeStart}
duration={nestedEntry.duration}
delay={nestedEntry.delay}
dayOffset={nestedEntry.dayOffset}
isLinkedToLoaded={isLinkedToLoaded}
isSelected={isSelected}
isPast={isPast}
@@ -36,7 +36,7 @@
display: flex;
gap: 0.5rem;
align-items: center;
background-color: color-mix(in srgb, var(--user-bg, transparent) 10%, transparent 90%);
background-color: color-mix(in srgb, var(--user-bg, transparent) 15%, transparent 85%);
}
.entryIndex {
+3 -3
View File
@@ -39,9 +39,9 @@ import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import { useEntryCopy } from '../../common/stores/entryCopyStore';
import { cloneEvent } from '../../common/utils/clone';
import BlockBlock from './block-block/BlockBlock';
import BlockEnd from './block-block/BlockEnd';
import QuickAddBlock from './quick-add-block/QuickAddBlock';
import BlockEnd from './rundown-block/BlockEnd';
import RundownBlock from './rundown-block/RundownBlock';
import { makeRundownMetadata, makeSortableList, moveDown, moveUp } from './rundown.utils';
import RundownEmpty from './RundownEmpty';
import { useEventSelection } from './useEventSelection';
@@ -445,7 +445,7 @@ export default function Rundown({ data }: RundownProps) {
/>
)}
{isOntimeBlock(entry) ? (
<BlockBlock
<RundownBlock
data={entry}
hasCursor={hasCursor}
collapsed={getIsCollapsed(entry.id)}
@@ -14,8 +14,8 @@ import useMemoisedFn from '../../common/hooks/useMemoisedFn';
import { useEmitLog } from '../../common/stores/logger';
import { cloneEvent } from '../../common/utils/clone';
import DelayBlock from './delay-block/DelayBlock';
import EventBlock from './event-block/EventBlock';
import RundownDelay from './rundown-delay/RundownDelay';
import RundownEvent from './rundown-event/RundownEvent';
import { useEventSelection } from './useEventSelection';
export type EventItemActions =
@@ -48,22 +48,21 @@ interface RundownEntryProps {
isLinkedToLoaded: boolean;
}
export default function RundownEntry(props: RundownEntryProps) {
const {
isPast,
data,
loaded,
hasCursor,
isNext,
previousEntryId,
previousEventId,
playback,
isRolling,
eventIndex,
isNextDay,
totalGap,
isLinkedToLoaded,
} = props;
export default function RundownEntry({
isPast,
data,
loaded,
hasCursor,
isNext,
previousEntryId,
previousEventId,
playback,
isRolling,
eventIndex,
isNextDay,
totalGap,
isLinkedToLoaded,
}: RundownEntryProps) {
const { emitError } = useEmitLog();
const { addEntry, updateEntry, batchUpdateEvents, deleteEntry, groupEntries, swapEvents } = useEntryActions();
const { selectedEvents, unselect, clearSelectedEvents } = useEventSelection();
@@ -167,7 +166,7 @@ export default function RundownEntry(props: RundownEntryProps) {
if (isOntimeEvent(data)) {
return (
<EventBlock
<RundownEvent
eventId={data.id}
eventIndex={eventIndex}
cue={data.cue}
@@ -201,7 +200,7 @@ export default function RundownEntry(props: RundownEntryProps) {
/>
);
} else if (isOntimeDelay(data)) {
return <DelayBlock data={data} hasCursor={hasCursor} />;
return <RundownDelay data={data} hasCursor={hasCursor} />;
}
return null;
}
@@ -10,7 +10,7 @@ import { useAppMode } from '../../common/stores/appModeStore';
import { handleLinks } from '../../common/utils/linkUtils';
import { cx } from '../../common/utils/styleUtils';
import RundownEventEditor from './event-editor/RundownEventEditor';
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
import FinderPlacement from './placements/FinderPlacement';
import RundownWrapper from './RundownWrapper';
@@ -63,7 +63,7 @@ function RundownExport() {
{!hideSideBar && (
<div className={style.side}>
<ErrorBoundary>
<RundownEventEditor />
<RundownEntryEditor />
</ErrorBoundary>
</div>
)}
@@ -21,7 +21,7 @@ describe('makeRundownMetadata()', () => {
block: {
id: 'block',
type: SupportedEntry.Block,
events: ['11', 'delay', '12', '13'],
entries: ['11', 'delay', '12', '13'],
colour: 'red',
} as OntimeBlock,
'11': {
@@ -209,7 +209,7 @@ describe('makeRundownMetadata()', () => {
id: 'block',
type: SupportedEntry.Block,
colour: 'red',
events: ['1', '2'],
entries: ['1', '2'],
} as OntimeBlock,
'1': {
id: '1',
@@ -288,12 +288,12 @@ describe('makeSortableList()', () => {
it('generates a list with block ends', () => {
const order = ['block-1', '2', 'block-3', 'block-4'];
const entries: RundownEntries = {
'block-1': { type: SupportedEntry.Block, id: 'block-1', events: ['11'] } as OntimeBlock,
'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: ['11'] } as OntimeBlock,
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
'2': { type: SupportedEntry.Event, id: '2', parent: null } as OntimeEvent,
'block-3': { type: SupportedEntry.Block, id: 'block-3', events: ['31'] } as OntimeBlock,
'block-3': { type: SupportedEntry.Block, id: 'block-3', entries: ['31'] } as OntimeBlock,
'31': { type: SupportedEntry.Event, id: '31', parent: 'block-3' } as OntimeEvent,
'block-4': { type: SupportedEntry.Block, id: 'block-4', events: [] as string[] } as OntimeBlock,
'block-4': { type: SupportedEntry.Block, id: 'block-4', entries: [] as string[] } as OntimeBlock,
};
const sortableList = makeSortableList(order, entries);
@@ -313,7 +313,7 @@ describe('makeSortableList()', () => {
it('closes dangling blocks', () => {
const order = ['block'];
const entries: RundownEntries = {
block: { type: SupportedEntry.Block, id: 'block-1', events: ['11', '12'] } as OntimeBlock,
block: { type: SupportedEntry.Block, id: 'block-1', entries: ['11', '12'] } as OntimeBlock,
'11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent,
'12': { type: SupportedEntry.Event, id: '12', parent: 'block-1' } as OntimeEvent,
};
@@ -325,8 +325,8 @@ describe('makeSortableList()', () => {
it('handles a list with a with just blocks', () => {
const order = ['block-1', 'block-2'];
const entries: RundownEntries = {
'block-1': { type: SupportedEntry.Block, id: 'block-1', events: [] as string[] } as OntimeBlock,
'block-2': { type: SupportedEntry.Block, id: 'block-2', events: [] as string[] } as OntimeBlock,
'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: [] as string[] } as OntimeBlock,
'block-2': { type: SupportedEntry.Block, id: 'block-2', entries: [] as string[] } as OntimeBlock,
};
const sortableList = makeSortableList(order, entries);
@@ -338,62 +338,62 @@ describe('moveUp()', () => {
const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3'];
const entries = {
event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent,
event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent,
block1: { type: 'block', id: 'block1', events: ['event3'] } as OntimeBlock,
event2: { type: 'event', id: 'event2', parent: null } as OntimeEvent,
block1: { type: 'block', id: 'block1', entries: ['event3'] } as OntimeBlock,
event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent,
block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock,
block2: { type: 'block', id: 'block2', entries: [] as EntryId[] } as OntimeBlock,
event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent,
};
it('moves an event up in the list', () => {
const result = moveUp('event2', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'event1', order: 'before', isBlock: false });
})
});
it.todo('disallows nesting blocks', () => {
const result = moveUp('block2', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'block1', order: 'before', isBlock: false });
})
});
it('moves an event into a block', () => {
const result = moveUp('event3', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'block2', order: 'insert', isBlock: true });
})
});
it('moving up from top is noop', () => {
const result = moveUp('event1', sortableData, entries);
expect(result).toMatchObject({ destinationId: null });
})
});
});
describe('moveDown()', () => {
const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3'];
const entries = {
event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent,
event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent,
block1: { type: 'block', id: 'block1', events: ['event11'] } as OntimeBlock,
event2: { type: 'event', id: 'event2', parent: null } as OntimeEvent,
block1: { type: 'block', id: 'block1', entries: ['event11'] } as OntimeBlock,
event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent,
block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock,
block2: { type: 'block', id: 'block2', entries: [] as EntryId[] } as OntimeBlock,
event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent,
};
it('moves an event down in the list', () => {
const result = moveDown('event1', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'event2', order: 'after', isBlock: false });
})
});
it.todo('disallows nesting blocks', () => {
const result = moveDown('block1', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'block2', order: 'before', isBlock: false });
})
});
it('moves an event into a block', () => {
const result = moveDown('event2', sortableData, entries);
expect(result).toStrictEqual({ destinationId: 'event11', order: 'before', isBlock: true });
})
});
it('moving down from bottom is noop', () => {
const result = moveDown('event3', sortableData, entries);
expect(result).toMatchObject({ destinationId: null });
})
});
});
});
@@ -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>
);
}
@@ -6,14 +6,13 @@ import { cx } from '../../../common/utils/styleUtils';
import EventEditor from './EventEditor';
import style from './EventEditor.module.scss';
import style from './EntryEditor.module.scss';
interface CuesheetEventEditorProps {
eventId: string;
}
export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
const { eventId } = props;
export default function CuesheetEventEditor({ eventId }: CuesheetEventEditorProps) {
const { data } = useRundown();
const [event, setEvent] = useState<OntimeEvent | null>(null);
@@ -37,7 +36,7 @@ export default function CuesheetEventEditor(props: CuesheetEventEditorProps) {
}
return (
<div className={cx([style.eventEditor, style.inModal])} data-testid='editor-container'>
<div className={cx([style.entryEditor, style.inModal])} data-testid='editor-container'>
<EventEditor event={event} />
</div>
);
@@ -1,4 +1,4 @@
.eventEditor {
.entryEditor {
max-height: 100%;
display: flex;
flex-direction: column;
@@ -88,3 +88,19 @@
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);
}
}
@@ -6,44 +6,38 @@ import AppLink from '../../../common/components/link/app-link/AppLink';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import EventCustom from './composite/EventEditorCustom';
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
import EventEditorTimes from './composite/EventEditorTimes';
import EventEditorTitles from './composite/EventEditorTitles';
import EventEditorTriggers from './composite/EventEditorTriggers';
import EventEditorEmpty from './EventEditorEmpty';
import style from './EventEditor.module.scss';
import style from './EntryEditor.module.scss';
// any of the titles + custom field labels
export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | string;
// any of the titles + colour + custom field labels
export type EventEditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | string;
interface EventEditorProps {
event: OntimeEvent;
}
export default function EventEditor(props: EventEditorProps) {
const { event } = props;
export default function EventEditor({ event }: EventEditorProps) {
const { data: customFields } = useCustomFields();
const { updateEntry } = useEntryActions();
const isEditor = window.location.pathname.includes('editor');
const handleSubmit = useCallback(
(field: EditorUpdateFields, value: string) => {
(field: EventEditorUpdateFields, value: string) => {
if (field.startsWith('custom-')) {
const fieldLabel = field.split('custom-')[1];
updateEntry({ id: event?.id, custom: { [fieldLabel]: value } });
updateEntry({ id: event.id, custom: { [fieldLabel]: value } });
} else {
updateEntry({ id: event?.id, [field]: value });
updateEntry({ id: event.id, [field]: value });
}
},
[event?.id, updateEntry],
[event.id, updateEntry],
);
if (!event) {
return <EventEditorEmpty />;
}
return (
<div className={style.content}>
<EventEditorTimes
@@ -55,7 +49,7 @@ export default function EventEditor(props: EventEditorProps) {
timeStrategy={event.timeStrategy}
linkStart={event.linkStart}
countToEnd={event.countToEnd}
delay={event.delay ?? 0}
delay={event.delay}
endAction={event.endAction}
timerType={event.timerType}
timeWarning={event.timeWarning}
@@ -75,7 +69,7 @@ export default function EventEditor(props: EventEditorProps) {
Custom Fields
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
</Editor.Title>
<EventCustom fields={customFields} handleSubmit={handleSubmit} event={event} />
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} event={event} />
</div>
<div className={style.column}>
<Editor.Title>
@@ -1,4 +1,4 @@
.eventEditor {
.entryEditor {
color: $label-gray;
height: 100%;
max-height: 100%;
@@ -46,3 +46,15 @@
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);
}
@@ -1,5 +1,4 @@
import { memo, PropsWithChildren } from 'react';
import { Kbd } from '@chakra-ui/react';
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
@@ -10,7 +9,7 @@ export default memo(EventEditorEmpty);
function EventEditorEmpty() {
return (
<div className={style.eventEditor} data-testid='editor-container'>
<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}>
@@ -172,3 +171,7 @@ function EventEditorEmpty() {
function AuxKey({ children }: PropsWithChildren) {
return <span className={style.divider}>{children}</span>;
}
function Kbd({ children }: PropsWithChildren) {
return <span className={style.kbd}>{children}</span>;
}
@@ -1,20 +1,21 @@
import { useEffect, useState } from 'react';
import { isOntimeEvent, OntimeEvent } from 'ontime-types';
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 EventEditorFooter from './composite/EventEditorFooter';
import BlockEditor from './BlockEditor';
import EventEditor from './EventEditor';
import EventEditorEmpty from './EventEditorEmpty';
import style from './EventEditor.module.scss';
import style from './EntryEditor.module.scss';
export default function RundownEventEditor() {
export default function RundownEntryEditor() {
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const { data } = useRundown();
const [event, setEvent] = useState<OntimeEvent | null>(null);
const [event, setEvent] = useState<OntimeEvent | OntimeBlock | null>(null);
useEffect(() => {
if (data.order.length === 0) {
@@ -29,7 +30,7 @@ export default function RundownEventEditor() {
}
const event = data.entries[selectedEventId];
if (event && isOntimeEvent(event)) {
if (event && !isOntimeDelay(event)) {
setEvent(event);
} else {
setEvent(null);
@@ -40,8 +41,16 @@ export default function RundownEventEditor() {
return <EventEditorEmpty />;
}
if (isOntimeBlock(event)) {
return (
<div className={style.entryEditor} data-testid='editor-container'>
<BlockEditor block={event} />
</div>
);
}
return (
<div className={style.eventEditor} data-testid='editor-container'>
<div className={style.entryEditor} data-testid='editor-container'>
<EventEditor event={event} />
<EventEditorFooter id={event.id} cue={event.cue} />
</div>
@@ -1,23 +1,26 @@
import { CSSProperties, Fragment } from 'react';
import { CustomFields, OntimeEvent } from 'ontime-types';
import { CustomFields, OntimeBlock, OntimeEvent } from 'ontime-types';
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
import { EditorUpdateFields } from '../EventEditor';
import { EventEditorUpdateFields } from '../EventEditor';
import EventEditorImage from './EventEditorImage';
import EventTextArea from './EventTextArea';
import EventTextInput from './EventTextInput';
import EntryEditorTextInput from './EventTextInput';
import style from '../EventEditor.module.scss';
import style from '../EntryEditor.module.scss';
interface EventEditorCustomProps {
interface EntryEditorCustomFieldsProps {
fields: CustomFields;
event: OntimeEvent;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
event: OntimeEvent | OntimeBlock;
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
}
export default function EventEditorCustom(props: EventEditorCustomProps) {
const { fields: customFields, handleSubmit, event } = props;
export default function EntryEditorCustomFields({
fields: customFields,
handleSubmit,
event,
}: EntryEditorCustomFieldsProps) {
return (
<Fragment>
{Object.keys(customFields).map((fieldKey) => {
@@ -44,7 +47,7 @@ export default function EventEditorCustom(props: EventEditorCustomProps) {
if (customFields[fieldKey].type === 'image') {
return (
<div key={key} className={style.customImage}>
<EventTextInput
<EntryEditorTextInput
key={key}
field={fieldName}
label={labelText}
@@ -9,11 +9,8 @@ interface EventEditorFooterProps {
cue: string;
}
export const EventEditorFooter = memo(_EventEditorFooter);
function _EventEditorFooter(props: EventEditorFooterProps) {
const { id, cue } = props;
export default memo(EventEditorFooter);
function EventEditorFooter({ id, cue }: EventEditorFooterProps) {
const loadById = `/ontime/load/id "${id}"`;
const loadByCue = `/ontime/load/cue "${cue}"`;
@@ -4,9 +4,7 @@ interface EventEditorImageProps {
src: string;
}
export default function EventEditorImage(props: EventEditorImageProps) {
const { src } = props;
export default function EventEditorImage({ src }: EventEditorImageProps) {
return (
<div className={style.imageContainer}>
<img loading='lazy' src={src} />
@@ -1,16 +1,18 @@
import { memo } from 'react';
import { IoInformationCircle } from 'react-icons/io5';
import { Select, Switch, Tooltip } from '@chakra-ui/react';
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 '../EventEditor.module.scss';
import style from '../EntryEditor.module.scss';
interface EventEditorTimesProps {
eventId: string;
@@ -30,26 +32,25 @@ interface EventEditorTimesProps {
type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'timeWarning' | 'timeDanger';
export default memo(EventEditorTimes);
function EventEditorTimes(props: EventEditorTimesProps) {
const {
eventId,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
countToEnd,
delay,
endAction,
timerType,
timeWarning,
timeDanger,
} = props;
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) });
updateEntry({ id: eventId, countToEnd: value as boolean });
return;
}
@@ -100,27 +101,22 @@ function EventEditorTimes(props: EventEditorTimesProps) {
<div>
<Editor.Label htmlFor='endAction'>End Action</Editor.Label>
<Select
id='endAction'
size='sm'
name='endAction'
value={endAction}
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
>
<option value={EndAction.None}>None</option>
<option value={EndAction.LoadNext}>Load next event</option>
<option value={EndAction.PlayNext}>Play next event</option>
</Select>
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'
size='md'
isChecked={countToEnd}
onChange={() => handleSubmit('countToEnd', countToEnd)}
variant='ontime'
checked={countToEnd}
onCheckedChange={(value) => handleSubmit('countToEnd', value)}
/>
{countToEnd ? 'On' : 'Off'}
</Editor.Label>
@@ -141,18 +137,15 @@ function EventEditorTimes(props: EventEditorTimesProps) {
<div>
<Editor.Label htmlFor='timerType'>Timer Type</Editor.Label>
<Select
size='sm'
id='timerType'
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>
<option value={TimerType.None}>None</option>
</Select>
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}>
@@ -1,15 +1,15 @@
import { memo } from 'react';
import { Input } from '@chakra-ui/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 { type EditorUpdateFields } from '../EventEditor';
import Input from '../../../../common/components/input/input/Input';
import { type EventEditorUpdateFields } from '../EventEditor';
import EventTextArea from './EventTextArea';
import EventTextInput from './EventTextInput';
import EntryEditorTextInput from './EventTextInput';
import style from '../EventEditor.module.scss';
import style from '../EntryEditor.module.scss';
interface EventEditorTitlesProps {
eventId: string;
@@ -17,12 +17,11 @@ interface EventEditorTitlesProps {
title: string;
note: string;
colour: string;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
}
const EventEditorTitles = (props: EventEditorTitlesProps) => {
const { eventId, cue, title, note, colour, handleSubmit } = props;
export default memo(EventEditorTitles);
function EventEditorTitles({ eventId, cue, title, note, colour, handleSubmit }: EventEditorTitlesProps) {
const cueSubmitHandler = (_field: string, newValue: string) => {
handleSubmit('cue', sanitiseCue(newValue));
};
@@ -33,25 +32,22 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
<div className={style.splitTwo}>
<div>
<Editor.Label htmlFor='eventId'>Event ID (read only)</Editor.Label>
<Input
id='eventId'
size='sm'
variant='ontime-filled'
data-testid='input-textfield'
value={eventId}
readOnly
/>
<Input id='eventId' data-testid='input-textfield' value={eventId} readOnly fluid />
</div>
<EventTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
<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>
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<EntryEditorTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
</div>
);
};
export default memo(EventEditorTitles);
}
@@ -1,9 +1,12 @@
import { Fragment, useCallback, useState } from 'react';
import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5';
import { Button, IconButton, Select, Tooltip } from '@chakra-ui/react';
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';
@@ -17,8 +20,7 @@ interface EventEditorTriggersProps {
triggers: Trigger[];
}
export default function EventEditorTriggers(props: EventEditorTriggersProps) {
const { triggers, eventId } = props;
export default function EventEditorTriggers({ triggers, eventId }: EventEditorTriggersProps) {
const showTriggers = triggers.length > 0;
return (
@@ -34,8 +36,7 @@ interface EventTriggerFormProps {
triggers?: Trigger[];
}
function EventTriggerForm(props: EventTriggerFormProps) {
const { eventId, triggers } = props;
function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
const { data: automationSettings } = useAutomationSettings();
const { updateEntry } = useEntryActions();
const [automationId, setAutomationId] = useState<string | undefined>(undefined);
@@ -68,38 +69,21 @@ function EventTriggerForm(props: EventTriggerFormProps) {
return (
<div className={style.triggerForm}>
<Select
size='sm'
variant='ontime'
value={cycleValue}
onChange={(e) => setCycleValue(e.target.value as TimerLifeCycle)}
>
<option disabled>Lifecycle Trigger</option>
{eventTriggerOptions.map((cycle) => (
<option key={cycle} value={cycle}>
{cycle}
</option>
))}
</Select>
placeholder='Choose a trigger'
onChange={(value) => setCycleValue(value)}
options={eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle }))}
/>
<Select
size='sm'
variant='ontime'
value={automationId}
defaultValue='«invalid»'
onChange={(e) => setAutomationId(e.target.value)}
>
<option disabled value='«invalid»'>
Automation
</option>
{Object.values(automationSettings.automations).map(({ id, title }) => (
<option key={id} value={id}>
{title}
</option>
))}
</Select>
placeholder='Choose an automation'
onChange={(value) => setAutomationId(value)}
options={Object.values(automationSettings.automations).map(({ id, title }) => ({ value: id, label: title }))}
/>
<Button
variant='ontime-subtle'
size='sm'
isDisabled={validationError !== undefined}
disabled={validationError !== undefined}
onClick={() => automationId && handleSubmit(cycleValue, automationId)}
>
Add
@@ -120,8 +104,7 @@ interface ExistingEventTriggersProps {
triggers: Trigger[];
}
function ExistingEventTriggers(props: ExistingEventTriggersProps) {
const { eventId, triggers } = props;
function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps) {
const { updateEntry } = useEntryActions();
const { data: automationSettings } = useAutomationSettings();
@@ -154,14 +137,9 @@ function ExistingEventTriggers(props: ExistingEventTriggersProps) {
<div key={id} className={style.trigger}>
<Tag>{triggerLifeCycle}</Tag>
<Tag>{automationTitle}</Tag>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
onClick={() => handleDelete(id)}
/>
<IconButton variant='subtle-destructive' onClick={() => handleDelete(id)}>
<IoTrash />
</IconButton>
</div>
);
})}
@@ -3,19 +3,25 @@ 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 { EditorUpdateFields } from '../EventEditor';
import { EventEditorUpdateFields } from '../EventEditor';
interface CountedTextAreaProps {
className?: string;
field: EditorUpdateFields;
field: EventEditorUpdateFields;
label: string;
initialValue: string;
style?: CSSProperties;
submitHandler: (field: EditorUpdateFields, value: string) => void;
submitHandler: (field: EventEditorUpdateFields, value: string) => void;
}
export default function EventTextArea(props: CountedTextAreaProps) {
const { className, field, label, initialValue, style: givenStyles, submitHandler } = props;
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]);
@@ -1,20 +1,29 @@
import { useCallback, useRef } from 'react';
import { Input, InputProps } from '@chakra-ui/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 { EditorUpdateFields } from '../EventEditor';
import { BlockEditorUpdateTextFields } from '../BlockEditor';
import { EventEditorUpdateFields } from '../EventEditor';
interface EventTextInputProps extends InputProps {
field: EditorUpdateFields;
interface EntryEditorTextInputProps extends InputProps {
field: EventEditorUpdateFields | BlockEditorUpdateTextFields;
label: string;
initialValue: string;
placeholder?: string;
submitHandler: (field: EditorUpdateFields, value: string) => void;
submitHandler: (field: EventEditorUpdateFields, value: string) => void;
}
export default function EventTextInput(props: EventTextInputProps) {
const { className, field, label, initialValue, style: givenStyles, submitHandler, maxLength, placeholder } = props;
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]);
@@ -30,16 +39,14 @@ export default function EventTextInput(props: EventTextInputProps) {
<Input
id={field}
ref={ref}
size='sm'
variant='ontime-filled'
maxLength={maxLength}
fluid
data-testid='input-textfield'
value={value}
maxLength={maxLength || 100}
placeholder={placeholder}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
autoComplete='off'
/>
</div>
);
@@ -5,5 +5,5 @@
padding-block: 0.5rem;
padding-left: calc(2em + 0.5rem);
background-color: color-mix(in srgb, var(--user-bg, transparent) 10%, transparent 90%);
background-color: color-mix(in srgb, var(--user-bg, transparent) 15%, transparent 85%);
}
@@ -14,9 +14,7 @@ interface QuickAddBlockProps {
}
export default memo(QuickAddBlock);
function QuickAddBlock(props: QuickAddBlockProps) {
const { previousEventId, parentBlock, backgroundColor } = props;
function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickAddBlockProps) {
const { addEntry } = useEntryActions();
const doLinkPrevious = useRef<HTMLInputElement | null>(null);
@@ -2,7 +2,7 @@
.blockEnd {
cursor: default;
height: 0.5rem;
height: 1rem;
background-color: var(--user-bg, $gray-1050);
border-radius: 0 0 $block-border-radius $block-border-radius;
@@ -8,8 +8,7 @@ interface BlockEndProps {
colour?: string;
}
export default function BlockEnd(props: BlockEndProps) {
const { id, colour } = props;
export default function BlockEnd({ id, colour }: BlockEndProps) {
const {
attributes: dragAttributes,
listeners: dragListeners,
@@ -37,6 +36,7 @@ export default function BlockEnd(props: BlockEndProps) {
...dragStyle,
...(colour ? { '--user-bg': colour } : {}),
}}
tabIndex={-1}
/>
);
}
@@ -26,9 +26,11 @@
color: $section-white;
font-size: 1rem;
display: grid;
justify-content: center;
padding-top: 0.25rem;
}
place-content: center;
position: relative;
cursor: pointer;
}
.header {
grid-area: header;
@@ -63,6 +65,8 @@
.drag {
@include drag-style;
position: absolute;
margin-top: 0.25rem;
&.isDragging {
cursor: grabbing;
@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { MouseEvent, useRef } from 'react';
import {
IoChevronDown,
IoChevronUp,
@@ -18,20 +18,21 @@ import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import { formatDuration, formatTime } from '../../../common/utils/time';
import EditableBlockTitle from '../common/EditableBlockTitle';
import { canDrop } from '../rundown.utils';
import { useEventSelection } from '../useEventSelection';
import style from './BlockBlock.module.scss';
import style from './RundownBlock.module.scss';
interface BlockBlockProps {
interface RundownBlockProps {
data: OntimeBlock;
hasCursor: boolean;
collapsed: boolean;
onCollapse: (collapsed: boolean, groupId: EntryId) => void;
}
export default function BlockBlock(props: BlockBlockProps) {
const { data, hasCursor, collapsed, onCollapse } = props;
export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: RundownBlockProps) {
const handleRef = useRef<null | HTMLSpanElement>(null);
const { clone, ungroup, deleteEntry } = useEntryActions();
const { selectedEvents, setSelectedBlock } = useEventSelection();
const [onContextMenu] = useContextMenu<HTMLDivElement>([
{
@@ -43,7 +44,7 @@ export default function BlockBlock(props: BlockBlockProps) {
label: 'Ungroup',
icon: IoFolderOpenOutline,
onClick: () => ungroup(data.id),
isDisabled: data.events.length === 0,
isDisabled: data.entries.length === 0,
},
{
label: 'Delete Block',
@@ -70,6 +71,20 @@ export default function BlockBlock(props: BlockBlockProps) {
animateLayoutChanges: () => false,
});
const handleFocusClick = (event: MouseEvent) => {
event.stopPropagation();
// event.button === 2 is a right-click
// disable selection if the user selected events and right clicks
// so the context menu shows up
if (selectedEvents.size > 1 && event.button === 2) {
return;
}
// UI indexes are 1 based
setSelectedBlock({ id: data.id });
};
const binderColours = data.colour && getAccessibleColour(data.colour);
const isValidDrop = over?.id && canDrop(over.data.current?.type, over.data.current?.parent);
@@ -84,6 +99,7 @@ export default function BlockBlock(props: BlockBlockProps) {
<div
className={cx([style.block, hasCursor && style.hasCursor, !collapsed && style.expanded])}
ref={setNodeRef}
onClick={handleFocusClick}
onContextMenu={onContextMenu}
style={{
...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
@@ -128,7 +144,7 @@ export default function BlockBlock(props: BlockBlockProps) {
</div>
<div className={style.metaEntry}>
<div>Events</div>
<div>{data.events.length}</div>
<div>{data.entries.length}</div>
</div>
</div>
</div>
@@ -9,15 +9,14 @@ import DelayInput from '../../../common/components/input/delay-input/DelayInput'
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx } from '../../../common/utils/styleUtils';
import style from './DelayBlock.module.scss';
import style from './RundownDelay.module.scss';
interface DelayBlockProps {
interface RundownDelayProps {
data: OntimeDelay;
hasCursor: boolean;
}
export default function DelayBlock(props: DelayBlockProps) {
const { data, hasCursor } = props;
export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
const { applyDelay, deleteEntry } = useEntryActions();
const handleRef = useRef<null | HTMLSpanElement>(null);
@@ -2,7 +2,7 @@
$skip-opacity: 0.2;
.eventBlock {
.rundownEvent {
@include block-styling;
background-color: $block-bg;
margin-block: 0.25rem;
@@ -19,12 +19,12 @@ import type { EventItemActions } from '../RundownEntry';
import { useEventIdSwapping } from '../useEventIdSwapping';
import { getSelectionMode, useEventSelection } from '../useEventSelection';
import EventBlockInner from './EventBlockInner';
import RundownEventInner from './RundownEventInner';
import RundownIndicators from './RundownIndicators';
import style from './EventBlock.module.scss';
import style from './RundownEvent.module.scss';
interface EventBlockProps {
interface RundownEventProps {
eventId: EntryId;
cue: string;
timeStart: number;
@@ -65,39 +65,38 @@ interface EventBlockProps {
hasTriggers: boolean;
}
export default function EventBlock(props: EventBlockProps) {
const {
eventId,
cue,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
countToEnd,
eventIndex,
endAction,
timerType,
title,
note,
delay,
colour,
isPast,
isNext,
skip = false,
parent,
loaded,
hasCursor,
playback,
isRolling,
gap,
isNextDay,
dayOffset,
totalGap,
isLinkedToLoaded,
actionHandler,
hasTriggers,
} = props;
export default function RundownEvent({
eventId,
cue,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
countToEnd,
eventIndex,
endAction,
timerType,
title,
note,
delay,
colour,
isPast,
isNext,
skip = false,
parent,
loaded,
hasCursor,
playback,
isRolling,
gap,
isNextDay,
dayOffset,
totalGap,
isLinkedToLoaded,
actionHandler,
hasTriggers,
}: RundownEventProps) {
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
const { selectedEvents, setSelectedEvents } = useEventSelection();
const handleRef = useRef<null | HTMLSpanElement>(null);
@@ -224,7 +223,7 @@ export default function EventBlock(props: EventBlockProps) {
const isSelected = selectedEvents.has(eventId);
const blockClasses = cx([
style.eventBlock,
style.rundownEvent,
skip ? style.skip : null,
isPast ? style.past : null,
loaded ? style.loaded : null,
@@ -268,7 +267,7 @@ export default function EventBlock(props: EventBlockProps) {
</div>
{isVisible && (
<EventBlockInner
<RundownEventInner
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
@@ -18,13 +18,13 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import TimeInputFlow from '../time-input-flow/TimeInputFlow';
import EventBlockChip from './composite/EventBlockChip';
import EventBlockPlayback from './composite/EventBlockPlayback';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
import RundownEventChip from './composite/RundownEventChip';
import EventBlockPlayback from './composite/RundownEventPlayback';
import EventBlockProgressBar from './composite/RundownEventProgressBar';
import style from './EventBlock.module.scss';
import style from './RundownEvent.module.scss';
interface EventBlockInnerProps {
interface RundownEventInnerProps {
eventId: string;
timeStart: number;
timeEnd: number;
@@ -50,32 +50,31 @@ interface EventBlockInnerProps {
hasTriggers: boolean;
}
function EventBlockInner(props: EventBlockInnerProps) {
const {
eventId,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
countToEnd,
endAction,
timerType,
title,
note,
delay,
isNext,
skip = false,
loaded,
playback,
isRolling,
dayOffset,
isPast,
totalGap,
isLinkedToLoaded,
hasTriggers,
} = props;
export default memo(RundownEventInner);
function RundownEventInner({
eventId,
timeStart,
timeEnd,
duration,
timeStrategy,
linkStart,
countToEnd,
endAction,
timerType,
title,
note,
delay,
isNext,
skip = false,
loaded,
playback,
isRolling,
dayOffset,
isPast,
totalGap,
isLinkedToLoaded,
hasTriggers,
}: RundownEventInnerProps) {
const [renderInner, setRenderInner] = useState(false);
useEffect(() => {
@@ -119,7 +118,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
disablePlayback={skip || isRolling}
/>
{!skip && (
<EventBlockChip
<RundownEventChip
className={style.chipSection}
id={eventId}
timeStart={timeStart}
@@ -164,8 +163,6 @@ function EventBlockInner(props: EventBlockInnerProps) {
);
}
export default memo(EventBlockInner);
function EndActionIcon(props: { action: EndAction; className: string }) {
const { action, className } = props;
const maybeActiveClasses = cx([action !== EndAction.None && style.active, className]);
@@ -1,4 +1,4 @@
import { formatDelay, formatGap } from './EventBlock.utils';
import { formatDelay, formatGap } from './rundownEvent.utils';
import style from './RundownIndicators.module.scss';
@@ -9,9 +9,7 @@ interface RundownIndicatorProps {
gap: number;
}
export default function RundownIndicators(props: RundownIndicatorProps) {
const { timeStart, delay, gap, isNextDay } = props;
export default function RundownIndicators({ timeStart, delay, gap, isNextDay }: RundownIndicatorProps) {
const hasGap = formatGap(gap, isNextDay);
const hasDelay = formatDelay(timeStart, delay);
@@ -1,4 +1,4 @@
import { formatDelay } from '../EventBlock.utils';
import { formatDelay } from '../rundownEvent.utils';
describe('formatDelay()', () => {
it('adds a given delay to the start time', () => {
@@ -9,9 +9,9 @@ import { cx } from '../../../../common/utils/styleUtils';
import { formatDuration, formatTime, useTimeUntilStart } from '../../../../common/utils/time';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import style from './EventBlockChip.module.scss';
import style from './RundownEventChip.module.scss';
interface EventBlockChipProps {
interface RundownEventChipProps {
id: string;
timeStart: number;
delay: number;
@@ -24,8 +24,18 @@ interface EventBlockChipProps {
isLinkedToLoaded: boolean;
}
export default function EventBlockChip(props: EventBlockChipProps) {
const { timeStart, delay, dayOffset, isPast, isLoaded, className, totalGap, id, duration, isLinkedToLoaded } = props;
export default function RundownEventChip({
timeStart,
delay,
dayOffset,
isPast,
isLoaded,
className,
totalGap,
id,
duration,
isLinkedToLoaded,
}: RundownEventChipProps) {
const { playback } = usePlayback();
if (isLoaded) {
@@ -6,7 +6,7 @@ import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import { setEventPlayback } from '../../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import style from '../EventBlock.module.scss';
import style from '../RundownEvent.module.scss';
const blockBtnStyle = {
size: 'sm',
@@ -23,7 +23,7 @@ const tooltipProps = {
openDelay: tooltipDelayMid,
};
interface EventBlockPlaybackProps {
interface RundownEventPlaybackProps {
eventId: string;
skip: boolean;
isPlaying: boolean;
@@ -32,8 +32,15 @@ interface EventBlockPlaybackProps {
disablePlayback: boolean;
}
const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props;
export default memo(RundownEventPlayback);
function RundownEventPlayback({
eventId,
skip,
isPlaying,
isPaused,
loaded,
disablePlayback,
}: RundownEventPlaybackProps) {
const { updateEntry } = useEntryActions();
const toggleSkip = (event: MouseEvent) => {
@@ -123,6 +130,4 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
/>
</div>
);
};
export default memo(EventBlockPlayback);
}
@@ -1,9 +1,9 @@
import { useTimer } from '../../../../common/hooks/useSocket';
import { getProgress } from '../../../../common/utils/getProgress';
import style from './EventBlockProgressBar.module.scss';
import style from './RundownEventProgressBar.module.scss';
export default function EventBlockProgressBar() {
export default function RundownEventProgressBar() {
const timer = useTimer();
const progress = getProgress(timer.current, timer.duration);
@@ -140,7 +140,7 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent
// inside a block there are delays and events
// there is no need for special handling
flatIds.push(entry.id);
flatIds.push(...entry.events);
flatIds.push(...entry.entries);
// close the block
flatIds.push(`end-${entry.id}`);
@@ -225,7 +225,7 @@ export function moveDown(entryId: EntryId, sortableData: EntryId[], entries: Run
return { destinationId: nextEntryId, order: 'after', isBlock: false };
}
const firstBlockChild = entries[nextEntryId].events.at(0);
const firstBlockChild = entries[nextEntryId].entries.at(0);
if (firstBlockChild) {
// 2. add before the first child of the block
return { destinationId: firstBlockChild, order: 'before', isBlock: true };
@@ -24,6 +24,7 @@ interface EventBlockTimerProps {
showLabels?: boolean;
}
export default memo(TimeInputFlow);
function TimeInputFlow(props: EventBlockTimerProps) {
const { eventId, countToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, showLabels } = props;
const { updateEntry, updateTimer } = useEntryActions();
@@ -135,5 +136,3 @@ function TimeInputFlow(props: EventBlockTimerProps) {
</>
);
}
export default memo(TimeInputFlow);
@@ -12,6 +12,8 @@ interface EventSelectionStore {
selectedEvents: Set<EntryId>;
anchoredIndex: MaybeNumber;
cursor: MaybeString;
entryMode: 'event' | 'block' | null;
setSelectedBlock: (selectionArgs: { id: EntryId }) => void;
setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
clearSelectedEvents: () => void;
clearMultiSelect: () => void;
@@ -22,13 +24,21 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
selectedEvents: new Set(),
anchoredIndex: null,
cursor: null,
setSelectedEvents: (selectionArgs) => {
const { id, index, selectMode } = selectionArgs;
const { selectedEvents, anchoredIndex } = get();
entryMode: null,
setSelectedBlock: ({ id }) => {
set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'block' });
},
setSelectedEvents: ({ id, index, selectMode }) => {
const { selectedEvents, anchoredIndex, entryMode } = get();
// if we are in block mode, we replace the selection and change the mode
if (entryMode === 'block') {
return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' });
}
// on click, we replace selection with event
if (selectMode === 'click') {
return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id });
return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' });
}
// on ctrl + click, we toggle the selection of that event
@@ -42,6 +52,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
selectedEvents: selectedEvents.add(id),
anchoredIndex: index,
cursor: id,
entryMode: 'event',
});
}
@@ -57,6 +68,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
return set({
selectedEvents,
anchoredIndex: nextIndex < 0 ? rundownData.order.length - 1 : nextIndex,
entryMode: 'event',
});
}
@@ -83,19 +95,23 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
return set({
selectedEvents: new Set([...selectedEvents, ...selectedEventIds]),
anchoredIndex: index,
entryMode: 'event',
});
}
},
clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null }),
clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }),
clearMultiSelect: () => {
const { selectedEvents } = get();
const [firstSelected] = selectedEvents;
set({ selectedEvents: new Set(firstSelected || undefined), anchoredIndex: null });
set({ selectedEvents: new Set(firstSelected || undefined), anchoredIndex: null, entryMode: null });
},
unselect: (id: string) => {
const { selectedEvents } = get();
const { entryMode, selectedEvents } = get();
selectedEvents.delete(id);
set({ selectedEvents });
set({
selectedEvents,
entryMode: selectedEvents.size === 0 ? null : entryMode,
});
},
}));
@@ -10,7 +10,7 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import { useFlatRundown } from '../../common/hooks-query/useRundown';
import { CuesheetOverview } from '../../features/overview/Overview';
import CuesheetEventEditor from '../../features/rundown/event-editor/CuesheetEventEditor';
import CuesheetEventEditor from '../../features/rundown/entry-editor/CuesheetEventEditor';
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
@@ -1,12 +1,15 @@
/* element attempts matching input styles */
/* element matching input styles */
.textInput {
height: 2rem;
background-color: transparent;
border-radius: 3px;
border-radius: $component-border-radius-md;
display: flex;
align-items: center;
gap: 0.25rem;
letter-spacing: 1px;
font-size: 1rem;
font-variant-numeric: tabular-nums;
&.delayed {
color: $ontime-delay-text;
@@ -10,7 +10,7 @@ const baseEvent = {
const baseBlock = {
type: SupportedEntry.Block,
events: [],
entries: [],
};
/**
@@ -570,7 +570,7 @@ describe('processRundown()', () => {
const rundown = makeRundown({
order: ['1'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['100', '200', '300'] }),
'1': makeOntimeBlock({ id: '1', entries: ['100', '200', '300'] }),
'100': makeOntimeEvent({ id: '100', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }),
'200': makeOntimeEvent({ id: '200', timeStart: 200, timeEnd: 300, duration: 100 }),
'300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }),
@@ -584,7 +584,7 @@ describe('processRundown()', () => {
expect(generatedRundown.entries).toMatchObject({
'1': {
type: SupportedEntry.Block,
events: ['100', '200', '300'],
entries: ['100', '200', '300'],
startTime: 100,
endTime: 400,
duration: 300,
@@ -601,15 +601,15 @@ describe('processRundown()', () => {
order: ['0', '1', '2', '3'],
entries: {
'0': makeOntimeEvent({ id: '0', timeStart: 0, timeEnd: 10, duration: 10, linkStart: false }),
'1': makeOntimeBlock({ id: '1', events: ['101', '102', '103'] }),
'1': makeOntimeBlock({ id: '1', entries: ['101', '102', '103'] }),
'101': makeOntimeEvent({ id: '101', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }),
'102': makeOntimeEvent({ id: '102', timeStart: 200, timeEnd: 300, duration: 100, linkStart: true }),
'103': makeOntimeEvent({ id: '103', timeStart: 300, timeEnd: 400, duration: 100, linkStart: true }),
'2': makeOntimeBlock({ id: '2', events: ['201', '202', '203'] }),
'2': makeOntimeBlock({ id: '2', entries: ['201', '202', '203'] }),
'201': makeOntimeEvent({ id: '201', timeStart: 500, timeEnd: 600, duration: 100, linkStart: false }),
'202': makeOntimeEvent({ id: '202', timeStart: 600, timeEnd: 700, duration: 100, linkStart: true }),
'203': makeOntimeEvent({ id: '203', timeStart: 700, timeEnd: 800, duration: 100, linkStart: true }),
'3': makeOntimeBlock({ id: '3', events: ['301', '302', '303'] }),
'3': makeOntimeBlock({ id: '3', entries: ['301', '302', '303'] }),
'301': makeOntimeEvent({ id: '301', timeStart: 900, timeEnd: 1000, duration: 100, linkStart: false }),
'302': makeOntimeEvent({ id: '302', timeStart: 1000, timeEnd: 1100, duration: 100, linkStart: true }),
'303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }),
@@ -624,7 +624,7 @@ describe('processRundown()', () => {
'0': { type: SupportedEntry.Event, parent: null },
'1': {
type: SupportedEntry.Block,
events: ['101', '102', '103'],
entries: ['101', '102', '103'],
startTime: 100,
endTime: 400,
duration: 300,
@@ -635,7 +635,7 @@ describe('processRundown()', () => {
'103': { parent: '1' },
'2': {
type: SupportedEntry.Block,
events: ['201', '202', '203'],
entries: ['201', '202', '203'],
startTime: 500,
endTime: 800,
duration: 300,
@@ -646,7 +646,7 @@ describe('processRundown()', () => {
'203': { id: '203', timeStart: 700, timeEnd: 800, duration: 100 },
'3': {
type: SupportedEntry.Block,
events: ['301', '302', '303'],
entries: ['301', '302', '303'],
startTime: 900,
endTime: 1200,
duration: 300,
@@ -789,7 +789,7 @@ describe('rundownMutation.remove()', () => {
const rundown = makeRundown({
order: ['1', '4'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['2', '3'] }),
'1': makeOntimeBlock({ id: '1', entries: ['2', '3'] }),
'2': makeOntimeEvent({ id: '2', parent: '1' }),
'3': makeOntimeDelay({ id: '3', parent: '1' }),
'4': makeOntimeEvent({ id: '4', parent: null }),
@@ -811,7 +811,7 @@ describe('rundownMutation.remove()', () => {
const rundown = makeRundown({
order: ['1', '4'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['2', '3'] }),
'1': makeOntimeBlock({ id: '1', entries: ['2', '3'] }),
'2': makeOntimeEvent({ id: '2', parent: '1' }),
'3': makeOntimeDelay({ id: '3', parent: '1' }),
'4': makeOntimeEvent({ id: '4', parent: null }),
@@ -823,7 +823,7 @@ describe('rundownMutation.remove()', () => {
expect(rundown.order).toStrictEqual(['1', '4']);
expect(rundown.entries).not.toHaveProperty('2');
expect(rundown.entries['1']).toMatchObject({
events: ['3'],
entries: ['3'],
});
});
});
@@ -853,7 +853,7 @@ describe('rundownMutation.reorder()', () => {
const rundown = makeRundown({
order: ['1', '2', '3'],
entries: {
'1': makeOntimeBlock({ id: '1', events: [] }),
'1': makeOntimeBlock({ id: '1', entries: [] }),
'2': makeOntimeEvent({ id: '2', parent: null }),
'3': makeOntimeEvent({ id: '3', parent: null }),
},
@@ -863,7 +863,7 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1', '2']);
expect(rundown.entries['1']).toMatchObject({
events: ['3'],
entries: ['3'],
});
expect(rundown.entries['3']).toMatchObject({
parent: '1',
@@ -875,7 +875,7 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '11', '2'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeEvent({ id: '2', parent: null }),
},
@@ -885,7 +885,7 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1']);
expect(rundown.entries['1']).toMatchObject({
events: ['2', '11'],
entries: ['2', '11'],
});
expect(rundown.entries['2']).toMatchObject({
parent: '1',
@@ -931,7 +931,7 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '11', '2'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeEvent({ id: '2', parent: null }),
},
@@ -941,7 +941,7 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1', '11', '2']);
expect(rundown.entries['1']).toMatchObject({
events: [],
entries: [],
});
expect(rundown.entries['2']).toMatchObject({
parent: null,
@@ -953,9 +953,9 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '11', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
@@ -964,10 +964,10 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1', '2']);
expect(rundown.entries['1']).toMatchObject({
events: [],
entries: [],
});
expect(rundown.entries['2']).toMatchObject({
events: ['11', '22'],
entries: ['11', '22'],
});
expect(rundown.entries['11']).toMatchObject({
parent: '2',
@@ -979,8 +979,8 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', events: [] }),
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
'1': makeOntimeBlock({ id: '1', entries: [] }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
@@ -989,10 +989,10 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1', '2']);
expect(rundown.entries['1']).toMatchObject({
events: ['22'],
entries: ['22'],
});
expect(rundown.entries['2']).toMatchObject({
events: [],
entries: [],
});
expect(rundown.entries['22']).toMatchObject({
parent: '1',
@@ -1004,9 +1004,9 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '11', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
@@ -1017,13 +1017,13 @@ describe('rundownMutation.reorder()', () => {
// expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']);
// expect(changeList).toStrictEqual(['1', '2', '11', '22']);
expect(rundown.entries['1']).toMatchObject({
events: ['11'],
entries: ['11'],
});
expect(rundown.entries['11']).toMatchObject({
parent: '1',
});
expect(rundown.entries['2']).toMatchObject({
events: [],
entries: [],
});
expect(rundown.entries['22']).toMatchObject({
parent: null,
@@ -1035,9 +1035,9 @@ describe('rundownMutation.reorder()', () => {
order: ['1', '2'],
flatOrder: ['1', '11', '2', '22'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['11'] }),
'1': makeOntimeBlock({ id: '1', entries: ['11'] }),
'11': makeOntimeEvent({ id: '11', parent: '1' }),
'2': makeOntimeBlock({ id: '2', events: ['22'] }),
'2': makeOntimeBlock({ id: '2', entries: ['22'] }),
'22': makeOntimeEvent({ id: '22', parent: '2' }),
},
});
@@ -1046,13 +1046,13 @@ describe('rundownMutation.reorder()', () => {
expect(rundown.order).toStrictEqual(['1', '11', '2']);
expect(rundown.entries['1']).toMatchObject({
events: [],
entries: [],
});
expect(rundown.entries['11']).toMatchObject({
parent: null,
});
expect(rundown.entries['2']).toMatchObject({
events: ['22'],
entries: ['22'],
});
expect(rundown.entries['22']).toMatchObject({
parent: '2',
@@ -1374,7 +1374,7 @@ describe('rundownMutation.applyDelay()', () => {
order: ['1', 'block', '2', '3'],
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
block: makeOntimeBlock({ id: 'block', events: ['delay'] }),
block: makeOntimeBlock({ id: 'block', entries: ['delay'] }),
delay: makeOntimeDelay({ id: 'delay', duration: 100, parent: 'block' }),
'2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 200, duration: 100, linkStart: true }),
'3': makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 300, duration: 100, linkStart: true }),
@@ -1417,7 +1417,7 @@ describe('rundownMutation.applyDelay()', () => {
entries: {
'1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
delay: makeOntimeDelay({ id: 'delay', duration: 100 }),
block: makeOntimeBlock({ id: 'block', events: ['block-1'] }),
block: makeOntimeBlock({ id: 'block', entries: ['block-1'] }),
'block-1': makeOntimeEvent({
id: 'block-1',
timeStart: 100,
@@ -1520,7 +1520,7 @@ describe('rundownMutation.clone()', () => {
const testRundown = makeRundown({
order: ['1'],
entries: {
'1': makeOntimeBlock({ id: '1', events: ['1a'] }),
'1': makeOntimeBlock({ id: '1', entries: ['1a'] }),
'1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }),
},
});
@@ -1528,7 +1528,7 @@ describe('rundownMutation.clone()', () => {
const newEntry = rundownMutation.clone(testRundown, testRundown.entries['1a']);
expect(testRundown.order).toStrictEqual(['1']);
expect(testRundown.entries['1']).toMatchObject({ events: ['1a', newEntry.id] });
expect(testRundown.entries['1']).toMatchObject({ entries: ['1a', newEntry.id] });
expect(testRundown.entries[newEntry.id]).toMatchObject({
type: SupportedEntry.Event,
parent: '1',
@@ -1540,7 +1540,7 @@ describe('rundownMutation.clone()', () => {
const testRundown = makeRundown({
order: ['1'],
entries: {
'1': makeOntimeBlock({ id: '1', title: 'top', events: ['1a'] }),
'1': makeOntimeBlock({ id: '1', title: 'top', entries: ['1a'] }),
'1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }),
},
});
@@ -1550,9 +1550,9 @@ describe('rundownMutation.clone()', () => {
expect(testRundown.order).toStrictEqual(['1', newEntry.id]);
expect(testRundown.entries[newEntry.id]).toMatchObject({
type: SupportedEntry.Block,
events: [expect.any(String)],
entries: [expect.any(String)],
});
expect((testRundown.entries[newEntry.id] as OntimeBlock).events[0]).not.toBe('1a');
expect((testRundown.entries[newEntry.id] as OntimeBlock).entries[0]).not.toBe('1a');
});
});
@@ -1575,7 +1575,7 @@ describe('rundownMutation.group()', () => {
expect(rundown.entries).toMatchObject({
[blockId]: {
type: SupportedEntry.Block,
events: ['1', '2'],
entries: ['1', '2'],
},
'1': { id: '1', type: SupportedEntry.Event, parent: blockId },
'2': { id: '2', type: SupportedEntry.Event, parent: blockId },
@@ -1590,7 +1590,7 @@ describe('rundownMutation.ungroup()', () => {
order: ['1', '2'],
entries: {
'1': makeOntimeEvent({ id: '1', cue: 'data1', parent: null }),
'2': makeOntimeBlock({ id: '2', events: ['21', '22'] }),
'2': makeOntimeBlock({ id: '2', entries: ['21', '22'] }),
'21': makeOntimeEvent({ id: '21', cue: 'data21', parent: '2' }),
'22': makeOntimeEvent({ id: '22', cue: 'data22', parent: '2' }),
},
@@ -47,7 +47,7 @@ describe('parseRundown()', () => {
flatOrder: ['1', '2', '3', '4'],
entries: {
'1': { id: '1', type: SupportedEntry.Event, title: 'test', skip: false } as OntimeEvent, // OK
'2': { id: '1', type: SupportedEntry.Block, title: 'test 2', skip: false } as OntimeBlock, // duplicate ID
'2': { id: '1', type: SupportedEntry.Block, title: 'test 2' } as OntimeBlock, // duplicate ID
'3': {} as OntimeEvent, // no data
'4': { id: '4', title: 'test 2', skip: false } as OntimeEvent, // no type
},
@@ -220,7 +220,7 @@ describe('parseRundown()', () => {
order: ['block'],
flatOrder: ['block'],
entries: {
block: makeOntimeBlock({ id: 'block', events: ['1', '2'] }),
block: makeOntimeBlock({ id: 'block', entries: ['1', '2'] }),
'1': makeOntimeEvent({ id: '1' }),
'2': makeOntimeEvent({ id: '2' }),
},
@@ -229,7 +229,7 @@ describe('parseRundown()', () => {
const parsedRundown = parseRundown(rundown, {});
expect(parsedRundown.order.length).toEqual(1);
expect(parsedRundown.entries.block).toMatchObject({ events: ['1', '2'] });
expect(parsedRundown.entries.block).toMatchObject({ entries: ['1', '2'] });
expect(Object.keys(parsedRundown.entries).length).toEqual(3);
});
});
+19 -19
View File
@@ -48,7 +48,7 @@ const cachedRundown: Rundown = {
id: '',
title: '',
order: [],
flatOrder: [], // TODO: remove in favour of the metadata flatEntryOrder
flatOrder: [],
entries: {},
revision: 0,
};
@@ -150,7 +150,7 @@ export function createTransaction(options: TransactionOptions): Transaction {
cachedRundown.title = rundown.title;
cachedRundown.entries = entries;
cachedRundown.order = order;
cachedRundown.flatOrder = metadata.flatEntryOrder; // TODO: remove in favour of the metadata flatEntryOrder
cachedRundown.flatOrder = metadata.flatEntryOrder;
customFieldsMetadata.assigned = assignedCustomFields;
rundownMetadata = metadata;
}
@@ -194,12 +194,12 @@ function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, pare
// 1. inserting an entry inside a block
const parentBlock = rundown.entries[parentId] as OntimeBlock;
if (afterId) {
const atEventsIndex = parentBlock.events.indexOf(afterId) + 1;
const atEventsIndex = parentBlock.entries.indexOf(afterId) + 1;
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
parentBlock.events = insertAtIndex(atEventsIndex, entry.id, parentBlock.events);
parentBlock.entries = insertAtIndex(atEventsIndex, entry.id, parentBlock.entries);
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
} else {
parentBlock.events = insertAtIndex(0, entry.id, parentBlock.events);
parentBlock.entries = insertAtIndex(0, entry.id, parentBlock.entries);
const atFlatIndex = rundown.flatOrder.indexOf(parentId) + 1;
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
}
@@ -247,8 +247,8 @@ function edit(rundown: Rundown, patch: PatchWithId): { entry: OntimeEntry; didIn
function remove(rundown: Rundown, entry: OntimeEntry) {
if (isOntimeBlock(entry)) {
// for ontime blocks, we need to iterate through the children and delete them
for (let i = 0; i < entry.events.length; i++) {
const nestedEntryId = entry.events[i];
for (let i = 0; i < entry.entries.length; i++) {
const nestedEntryId = entry.entries[i];
deleteEntry(nestedEntryId);
}
} else if (entry.parent) {
@@ -256,8 +256,8 @@ function remove(rundown: Rundown, entry: OntimeEntry) {
const parentBlock = rundown.entries[entry.parent] as OntimeBlock;
if (parentBlock) {
// we call a mutation to the parent event to remove the entry from the events
const filteredEvents = deleteById(parentBlock.events, entry.id);
edit(rundown, { id: parentBlock.id, events: filteredEvents });
const filteredEvents = deleteById(parentBlock.entries, entry.id);
edit(rundown, { id: parentBlock.id, entries: filteredEvents });
}
}
deleteEntry(entry.id);
@@ -300,8 +300,8 @@ function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry,
eventFrom.parent = toParent;
}
const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).events;
const destinationArray = toParent === null ? rundown.order : (rundown.entries[toParent] as OntimeBlock).events;
const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).entries;
const destinationArray = toParent === null ? rundown.order : (rundown.entries[toParent] as OntimeBlock).entries;
const fromIndex = sourceArray.indexOf(eventFrom.id);
const toIndex = (() => {
@@ -442,8 +442,8 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry {
const newBlock = cloneBlock(entry, getUniqueId(rundown));
const nestedIds: EntryId[] = [];
for (let i = 0; i < entry.events.length; i++) {
const nestedEntryId = entry.events[i];
for (let i = 0; i < entry.entries.length; i++) {
const nestedEntryId = entry.entries[i];
const nestedEntry = rundown.entries[nestedEntryId];
if (!nestedEntry) {
continue;
@@ -461,7 +461,7 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry {
// indexes + 1 since we are inserting after the cloned block
const atIndex = rundown.order.indexOf(entry.id) + 1;
newBlock.events = nestedIds;
newBlock.entries = nestedIds;
newBlock.title = `${entry.title || 'Untitled'} (copy)`;
rundown.entries[newBlock.id] = newBlock;
@@ -504,7 +504,7 @@ function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock {
rundown.order = rundown.order.filter((id) => id !== entryId);
}
newBlock.events = nestedEvents;
newBlock.entries = nestedEvents;
const insertIndex = Math.max(0, firstIndex);
// we have filtered the items from the order
// we will insert them now, with only the block at top level ...
@@ -519,7 +519,7 @@ function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock {
*/
function ungroup(rundown: Rundown, block: OntimeBlock) {
// get the events from the block and merge them into the order where the block was
const nestedEvents = block.events;
const nestedEvents = block.entries;
const blockIndex = rundown.order.indexOf(block.id);
rundown.order.splice(blockIndex, 1, ...nestedEvents);
@@ -714,8 +714,8 @@ export function processRundown(
const blockEvents: EntryId[] = [];
// check if the block contains nested entries
for (let j = 0; j < processedEntry.events.length; j++) {
const nestedEntryId = processedEntry.events[j];
for (let j = 0; j < processedEntry.entries.length; j++) {
const nestedEntryId = processedEntry.entries[j];
const nestedEntry = initialRundown.entries[nestedEntryId];
if (!nestedEntry) {
@@ -750,7 +750,7 @@ export function processRundown(
processedEntry.startTime = blockStartTime;
processedEntry.endTime = blockEndTime;
processedEntry.isFirstLinked = isFirstLinked;
processedEntry.events = blockEvents;
processedEntry.entries = blockEvents;
}
}
@@ -116,8 +116,8 @@ export function parseRundown(
} else if (isOntimeDelay(event)) {
newEvent = { ...delayDef, duration: event.duration, id };
} else if (isOntimeBlock(event)) {
for (let i = 0; i < event.events.length; i++) {
const nestedEventId = event.events[i];
for (let i = 0; i < event.entries.length; i++) {
const nestedEventId = event.entries[i];
const nestedEvent = rundown.entries[nestedEventId];
if (isOntimeEvent(nestedEvent)) {
@@ -149,8 +149,8 @@ export function parseRundown(
...blockDef,
title: event.title,
note: event.note,
events: event.events?.filter((eventId) => Object.hasOwn(rundown.entries, eventId)) ?? [],
skip: event.skip,
entries: event.entries?.filter((eventId) => Object.hasOwn(rundown.entries, eventId)) ?? [],
isNextDay: event.isNextDay,
colour: event.colour,
custom: { ...event.custom },
id,
@@ -99,19 +99,15 @@ router.patch('/swap', rundownSwapValidator, async (req: Request, res: Response<R
}
});
router.patch(
'/applydelay/:id',
paramsWithId,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await applyDelay(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.patch('/applydelay/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await applyDelay(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
router.post('/clone/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
@@ -133,19 +129,15 @@ router.post('/group', rundownArrayOfIds, async (req: Request, res: Response<Rund
}
});
router.post(
'/ungroup/:id',
paramsWithId,
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await ungroupEntries(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
},
);
router.post('/ungroup/:id', paramsWithId, async (req: Request, res: Response<Rundown | ErrorResponse>) => {
try {
const newRundown = await ungroupEntries(req.params.id);
res.status(200).send(newRundown);
} catch (error) {
const message = getErrorMessage(error);
res.status(400).send({ message });
}
});
router.delete('/', rundownArrayOfIds, async (req: Request, res: Response<MessageResponse | ErrorResponse>) => {
try {
@@ -347,7 +347,7 @@ export async function cloneEntry(entryId: EntryId): Promise<Rundown> {
// notify timer and external services of change
if (isOntimeBlock(newEntry)) {
notifyChanges(rundownMetadata, revision, { timer: newEntry.events, external: true });
notifyChanges(rundownMetadata, revision, { timer: newEntry.entries, external: true });
} else if (isOntimeEvent(newEntry)) {
notifyChanges(rundownMetadata, revision, { timer: [newEntry.id], external: true });
} else if (isOntimeDelay(newEntry)) {
@@ -156,8 +156,9 @@ export function createBlock(patch?: Partial<OntimeBlock>): OntimeBlock {
type: SupportedEntry.Block,
title: patch.title ?? '',
note: patch.note ?? '',
events: patch.events ?? [],
skip: patch.skip ?? false,
entries: patch.entries ?? [],
isNextDay: patch.isNextDay ?? false,
targetDuration: patch.targetDuration ?? null,
colour: makeString(patch.colour, ''),
custom: patch.custom ?? {},
revision: 0,
@@ -288,7 +289,7 @@ export function cloneBlock(entry: OntimeBlock, newId: EntryId): OntimeBlock {
newEntry.id = newId;
// in blocks, we need to remove the events references
newEntry.events = [];
newEntry.entries = [];
newEntry.revision = 0;
return newEntry;
}
+9 -6
View File
@@ -41,11 +41,12 @@ export const demoDb: DatabaseModel = {
entries: {
block: {
type: SupportedEntry.Block,
events: ['32d31', '21cd2', '0b371', '3cd28', 'e457f'],
entries: ['32d31', '21cd2', '0b371', '3cd28', 'e457f'],
id: 'block',
title: 'Test Block',
note: '',
skip: false,
isNextDay: false,
targetDuration: null,
colour: 'hotpink',
revision: 0,
startTime: null,
@@ -210,8 +211,9 @@ export const demoDb: DatabaseModel = {
title: 'Lunch break',
note: '',
colour: '',
events: [],
skip: false,
entries: [],
isNextDay: false,
targetDuration: null,
custom: {},
revision: 0,
startTime: null,
@@ -372,8 +374,9 @@ export const demoDb: DatabaseModel = {
title: 'Afternoon break',
note: '',
colour: '',
events: [],
skip: false,
entries: [],
isNextDay: false,
targetDuration: null,
custom: {},
revision: 0,
startTime: null,
+3 -2
View File
@@ -44,8 +44,9 @@ export const block: Omit<OntimeBlock, 'id'> = {
type: SupportedEntry.Block,
title: '',
note: '',
events: [],
skip: false,
entries: [],
isNextDay: false,
targetDuration: null,
colour: '',
custom: {},
// !==== RUNTIME METADATA ====! //
@@ -375,9 +375,9 @@ describe('loadBlock', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
1: makeOntimeBlock({ id: '1', events: ['11'] }),
1: makeOntimeBlock({ id: '1', entries: ['11'] }),
11: makeOntimeEvent({ id: '11', parent: '1' }),
2: makeOntimeBlock({ id: '2', events: [] }),
2: makeOntimeBlock({ id: '2', entries: [] }),
3: makeOntimeEvent({ id: '3', parent: null }),
},
order: ['0', '1', '2', '3'],
@@ -400,9 +400,9 @@ describe('loadBlock', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
1: makeOntimeBlock({ id: '1', events: ['11'] }),
1: makeOntimeBlock({ id: '1', entries: ['11'] }),
11: makeOntimeEvent({ id: '11', parent: '1' }),
2: makeOntimeBlock({ id: '2', events: ['22'] }),
2: makeOntimeBlock({ id: '2', entries: ['22'] }),
22: makeOntimeEvent({ id: '22', parent: '2' }),
},
order: ['0', '1', '2'],
@@ -425,9 +425,9 @@ describe('loadBlock', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeEvent({ id: '0', parent: null }),
1: makeOntimeBlock({ id: '1', events: ['11'] }),
1: makeOntimeBlock({ id: '1', entries: ['11'] }),
11: makeOntimeEvent({ id: '11', parent: '1' }),
2: makeOntimeBlock({ id: '2', events: ['22'] }),
2: makeOntimeBlock({ id: '2', entries: ['22'] }),
22: makeOntimeEvent({ id: '22', parent: '2' }),
},
order: ['0', '1', '2'],
@@ -452,7 +452,7 @@ describe('loadBlock', () => {
test('from block to same block will keep startedAt', () => {
const rundown = makeRundown({
entries: {
0: makeOntimeBlock({ id: '0', events: ['1', '2'] }),
0: makeOntimeBlock({ id: '0', entries: ['1', '2'] }),
1: makeOntimeEvent({ id: '1', parent: '0' }),
2: makeOntimeEvent({ id: '2', parent: '0' }),
},
+2 -2
View File
@@ -168,7 +168,7 @@
"title": "Lunch break",
"note": "",
"colour": "",
"events": [],
"entries": [],
"skip": false,
"custom": {},
"revision": 0,
@@ -323,7 +323,7 @@
"title": "Afternoon break",
"note": "",
"colour": "",
"events": [],
"entries": [],
"skip": false,
"custom": {},
"revision": 0,
+2 -2
View File
@@ -186,7 +186,7 @@
"title": "Lunch break",
"note": "",
"colour": "",
"events": [],
"entries": [],
"skip": false,
"custom": {},
"revision": 0,
@@ -341,7 +341,7 @@
"title": "Afternoon break",
"note": "",
"colour": "",
"events": [],
"entries": [],
"skip": false,
"custom": {},
"revision": 0,
@@ -23,8 +23,9 @@ export type OntimeBlock = OntimeBaseEvent & {
type: SupportedEntry.Block;
title: string;
note: string;
events: EntryId[];
skip: boolean;
entries: EntryId[];
isNextDay: boolean;
targetDuration: MaybeNumber;
colour: string;
custom: EntryCustomFields;
// !==== RUNTIME METADATA ====! //
@@ -340,7 +340,7 @@ describe('getLastEvent', () => {
const testRundown = {
entries: {
1: { id: '1', type: SupportedEntry.Event } as OntimeEvent,
block: { id: 'block', type: SupportedEntry.Block, events: ['21', '22', '23'] } as OntimeBlock,
block: { id: 'block', type: SupportedEntry.Block, entries: ['21', '22', '23'] } as OntimeBlock,
21: { id: '21', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent,
22: { id: '22', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent,
23: { id: '23', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent,