mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-08 17:03:53 +00:00
feat: edit groups
This commit is contained in:
committed by
Carlos Valente
parent
4c08482258
commit
473f50b493
@@ -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];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-4
@@ -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>
|
||||
);
|
||||
+17
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
-17
@@ -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>
|
||||
+13
-1
@@ -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);
|
||||
}
|
||||
+5
-2
@@ -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>;
|
||||
}
|
||||
+16
-7
@@ -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>
|
||||
+13
-10
@@ -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}
|
||||
+2
-5
@@ -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}"`;
|
||||
|
||||
+1
-3
@@ -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} />
|
||||
+36
-43
@@ -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}>
|
||||
+17
-21
@@ -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);
|
||||
}
|
||||
+21
-43
@@ -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>
|
||||
);
|
||||
})}
|
||||
+11
-5
@@ -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]);
|
||||
|
||||
+18
-11
@@ -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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+2
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -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;
|
||||
+23
-7
@@ -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>
|
||||
+3
-4
@@ -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);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
$skip-opacity: 0.2;
|
||||
|
||||
.eventBlock {
|
||||
.rundownEvent {
|
||||
@include block-styling;
|
||||
background-color: $block-bg;
|
||||
margin-block: 0.25rem;
|
||||
+37
-38
@@ -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}
|
||||
+31
-34
@@ -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]);
|
||||
+2
-4
@@ -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
-1
@@ -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', () => {
|
||||
+14
-4
@@ -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) {
|
||||
+12
-7
@@ -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);
|
||||
}
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+5
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+2
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user