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