From 473f50b4939a98211f5e375ca58bf5e4b111c76f Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Wed, 25 Jun 2025 06:10:12 +0200 Subject: [PATCH] feat: edit groups --- .../components/input/input/Input.module.scss | 8 +- .../common/components/input/input/Input.tsx | 9 +- .../input/time-input/NullableTimeInput.tsx | 135 ++++++++++++++++ .../input/time-input/TimeInput.module.scss | 1 - .../components/input/time-input/TimeInput.tsx | 19 +-- .../components/select/Select.module.scss | 16 +- .../src/common/components/select/Select.tsx | 1 + .../components/switch/Switch.module.scss | 52 +++++++ .../src/common/components/switch/Switch.tsx | 17 ++ .../client/src/common/hooks/useEntryAction.ts | 2 +- .../client/src/features/operator/Operator.tsx | 28 ++-- .../src/features/rundown/Rundown.module.scss | 2 +- apps/client/src/features/rundown/Rundown.tsx | 6 +- .../src/features/rundown/RundownEntry.tsx | 39 +++-- .../src/features/rundown/RundownExport.tsx | 4 +- .../rundown/__tests__/rundown.utils.test.ts | 46 +++--- .../rundown/entry-editor/BlockEditor.tsx | 147 ++++++++++++++++++ .../CuesheetEventEditor.tsx | 7 +- .../EntryEditor.module.scss} | 18 ++- .../EventEditor.tsx | 28 ++-- .../EventEditorEmpty.module.scss | 14 +- .../EventEditorEmpty.tsx | 7 +- .../RundownEntryEditor.tsx} | 23 ++- .../composite/EventEditorCustomFields.tsx} | 23 +-- .../composite/EventEditorFooter.module.scss | 0 .../composite/EventEditorFooter.tsx | 7 +- .../composite/EventEditorImage.module.scss | 0 .../composite/EventEditorImage.tsx | 4 +- .../composite/EventEditorTimes.tsx | 79 +++++----- .../composite/EventEditorTitles.tsx | 38 ++--- .../composite/EventEditorTriggers.module.scss | 0 .../composite/EventEditorTriggers.tsx | 64 +++----- .../composite/EventTextArea.tsx | 16 +- .../composite/EventTextInput.tsx | 29 ++-- .../composite/eventTrigger.constants.ts | 0 .../quick-add-block/QuickAddBlock.module.scss | 2 +- .../rundown/quick-add-block/QuickAddBlock.tsx | 4 +- .../BlockEnd.module.scss | 2 +- .../BlockEnd.tsx | 4 +- .../RundownBlock.module.scss} | 10 +- .../RundownBlock.tsx} | 30 +++- .../RundownDelay.module.scss} | 0 .../RundownDelay.tsx} | 7 +- .../RundownEvent.module.scss} | 2 +- .../RundownEvent.tsx} | 75 +++++---- .../RundownEventInner.tsx} | 65 ++++---- .../RundownIndicators.module.scss | 0 .../RundownIndicators.tsx | 6 +- .../__tests__/rundownEvent.utils.test.ts} | 2 +- .../composite/RundownEventChip.module.scss} | 0 .../composite/RundownEventChip.tsx} | 18 ++- .../composite/RundownEventPlayback.tsx} | 19 ++- .../RundownEventProgressBar.module.scss} | 0 .../composite/RundownEventProgressBar.tsx} | 4 +- .../rundownEvent.utils.ts} | 0 .../src/features/rundown/rundown.utils.ts | 4 +- .../rundown/time-input-flow/TimeInputFlow.tsx | 3 +- .../src/features/rundown/useEventSelection.ts | 32 +++- .../src/views/cuesheet/CuesheetPage.tsx | 2 +- .../TextLikeInput.module.scss | 7 +- .../rundown/__mocks__/rundown.mocks.ts | 2 +- .../rundown/__tests__/rundown.dao.test.ts | 84 +++++----- .../rundown/__tests__/rundown.parser.test.ts | 6 +- .../src/api-data/rundown/rundown.dao.ts | 38 ++--- .../src/api-data/rundown/rundown.parser.ts | 8 +- .../src/api-data/rundown/rundown.router.ts | 44 +++--- .../src/api-data/rundown/rundown.service.ts | 2 +- .../src/api-data/rundown/rundown.utils.ts | 7 +- apps/server/src/models/demoProject.ts | 15 +- apps/server/src/models/eventsDefinition.ts | 5 +- .../src/stores/__tests__/runtimeState.test.ts | 14 +- apps/server/test-db/db.json | 4 +- e2e/tests/fixtures/e2e-test-db.json | 4 +- .../src/definitions/core/OntimeEvent.type.ts | 5 +- .../src/rundown-utils/rundownUtils.test.ts | 2 +- 75 files changed, 916 insertions(+), 511 deletions(-) create mode 100644 apps/client/src/common/components/input/time-input/NullableTimeInput.tsx create mode 100644 apps/client/src/common/components/switch/Switch.module.scss create mode 100644 apps/client/src/common/components/switch/Switch.tsx create mode 100644 apps/client/src/features/rundown/entry-editor/BlockEditor.tsx rename apps/client/src/features/rundown/{event-editor => entry-editor}/CuesheetEventEditor.tsx (79%) rename apps/client/src/features/rundown/{event-editor/EventEditor.module.scss => entry-editor/EntryEditor.module.scss} (82%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/EventEditor.tsx (74%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/EventEditorEmpty.module.scss (69%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/EventEditorEmpty.tsx (96%) rename apps/client/src/features/rundown/{event-editor/RundownEventEditor.tsx => entry-editor/RundownEntryEditor.tsx} (59%) rename apps/client/src/features/rundown/{event-editor/composite/EventEditorCustom.tsx => entry-editor/composite/EventEditorCustomFields.tsx} (77%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorFooter.module.scss (100%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorFooter.tsx (79%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorImage.module.scss (100%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorImage.tsx (72%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorTimes.tsx (73%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorTitles.tsx (57%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorTriggers.module.scss (100%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventEditorTriggers.tsx (73%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventTextArea.tsx (79%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/EventTextInput.tsx (60%) rename apps/client/src/features/rundown/{event-editor => entry-editor}/composite/eventTrigger.constants.ts (100%) rename apps/client/src/features/rundown/{block-block => rundown-block}/BlockEnd.module.scss (91%) rename apps/client/src/features/rundown/{block-block => rundown-block}/BlockEnd.tsx (89%) rename apps/client/src/features/rundown/{block-block/BlockBlock.module.scss => rundown-block/RundownBlock.module.scss} (90%) rename apps/client/src/features/rundown/{block-block/BlockBlock.tsx => rundown-block/RundownBlock.tsx} (81%) rename apps/client/src/features/rundown/{delay-block/DelayBlock.module.scss => rundown-delay/RundownDelay.module.scss} (100%) rename apps/client/src/features/rundown/{delay-block/DelayBlock.tsx => rundown-delay/RundownDelay.tsx} (92%) rename apps/client/src/features/rundown/{event-block/EventBlock.module.scss => rundown-event/RundownEvent.module.scss} (99%) rename apps/client/src/features/rundown/{event-block/EventBlock.tsx => rundown-event/RundownEvent.tsx} (91%) rename apps/client/src/features/rundown/{event-block/EventBlockInner.tsx => rundown-event/RundownEventInner.tsx} (86%) rename apps/client/src/features/rundown/{event-block => rundown-event}/RundownIndicators.module.scss (100%) rename apps/client/src/features/rundown/{event-block => rundown-event}/RundownIndicators.tsx (70%) rename apps/client/src/features/rundown/{event-block/__tests__/EventBlock.utils.test.ts => rundown-event/__tests__/rundownEvent.utils.test.ts} (83%) rename apps/client/src/features/rundown/{event-block/composite/EventBlockChip.module.scss => rundown-event/composite/RundownEventChip.module.scss} (100%) rename apps/client/src/features/rundown/{event-block/composite/EventBlockChip.tsx => rundown-event/composite/RundownEventChip.tsx} (93%) rename apps/client/src/features/rundown/{event-block/composite/EventBlockPlayback.tsx => rundown-event/composite/RundownEventPlayback.tsx} (92%) rename apps/client/src/features/rundown/{event-block/composite/EventBlockProgressBar.module.scss => rundown-event/composite/RundownEventProgressBar.module.scss} (100%) rename apps/client/src/features/rundown/{event-block/composite/EventBlockProgressBar.tsx => rundown-event/composite/RundownEventProgressBar.tsx} (73%) rename apps/client/src/features/rundown/{event-block/EventBlock.utils.ts => rundown-event/rundownEvent.utils.ts} (100%) diff --git a/apps/client/src/common/components/input/input/Input.module.scss b/apps/client/src/common/components/input/input/Input.module.scss index ac3f3c970..c0ab1a5b0 100644 --- a/apps/client/src/common/components/input/input/Input.module.scss +++ b/apps/client/src/common/components/input/input/Input.module.scss @@ -9,12 +9,13 @@ border: 1px solid transparent; padding-inline: 0.5em; + outline: none; &:hover:not(:disabled) { background-color: $gray-1100; } - &:focus { + &:focus:not(:read-only) { background-color: $gray-1000; border: 1px solid $blue-500; } @@ -26,6 +27,7 @@ &::placeholder { color: $gray-500; + letter-spacing: 0; } } @@ -36,3 +38,7 @@ .large { height: 2.5rem; } + +.fluid { + width: 100%; +} diff --git a/apps/client/src/common/components/input/input/Input.tsx b/apps/client/src/common/components/input/input/Input.tsx index c3f9e1524..e72f84bd5 100644 --- a/apps/client/src/common/components/input/input/Input.tsx +++ b/apps/client/src/common/components/input/input/Input.tsx @@ -4,20 +4,23 @@ import { cx } from '../../../utils/styleUtils'; import style from './Input.module.scss'; -interface InputProps extends InputHTMLAttributes { +export interface InputProps extends InputHTMLAttributes { variant?: 'subtle'; height?: 'medium' | 'large'; + fluid?: boolean; } const Input = forwardRef(function Input( - { className, variant = 'subtle', height = 'medium', ...inputProps }, + { className, variant = 'subtle', height = 'medium', fluid, ...inputProps }, ref, ) { return ( ); diff --git a/apps/client/src/common/components/input/time-input/NullableTimeInput.tsx b/apps/client/src/common/components/input/time-input/NullableTimeInput.tsx new file mode 100644 index 000000000..8c65385fe --- /dev/null +++ b/apps/client/src/common/components/input/time-input/NullableTimeInput.tsx @@ -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 { + 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(props: NullableTimeInputProps) { + const { id, name, submitHandler, time, emptyDisplay, placeholder, disabled, align = 'center', className } = props; + const inputRef = useRef(null); + const [value, setValue] = useState(''); + 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) => { + if (event.key === 'Enter') { + inputRef.current?.blur(); + } + if (event.key === 'Escape') { + ignoreChange.current = true; + inputRef.current?.blur(); + resetValue(); + } + }, + [resetValue], + ); + + const onBlurHandler = useCallback( + (event: FocusEvent) => { + if (ignoreChange.current) { + ignoreChange.current = false; + return; + } + validateAndSubmit((event.target as HTMLInputElement).value); + }, + [validateAndSubmit], + ); + + useEffect(() => { + resetValue(); + }, [resetValue]); + + return ( + setValue(event.target.value)} + onBlur={onBlurHandler} + onKeyDown={onKeyDownHandler} + value={value} + maxLength={8} + autoComplete='off' + style={{ + textAlign: align, + }} + /> + ); +} diff --git a/apps/client/src/common/components/input/time-input/TimeInput.module.scss b/apps/client/src/common/components/input/time-input/TimeInput.module.scss index 2d4cae896..4a90a8a0e 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.module.scss +++ b/apps/client/src/common/components/input/time-input/TimeInput.module.scss @@ -2,6 +2,5 @@ width: 100%; max-width: 7.5em; letter-spacing: 1px; - font-size: 1rem; font-variant-numeric: tabular-nums; } diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index 1dcd16d5b..64fd5f73a 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -1,7 +1,6 @@ import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'; import { millisToString, parseUserTime } from 'ontime-utils'; -import { useEmitLog } from '../../../stores/logger'; import { cx } from '../../../utils/styleUtils'; import Input from '../input/Input'; @@ -19,8 +18,7 @@ interface TimeInputProps { } export default function TimeInput(props: TimeInputProps) { - const { id, name, submitHandler, time = 0, placeholder, disabled, align = 'center', className } = props; - const { emitError } = useEmitLog(); + const { id, name, submitHandler, time, placeholder, disabled, align = 'center', className } = props; const inputRef = useRef(null); const [value, setValue] = useState(''); const ignoreChange = useRef(false); @@ -29,16 +27,12 @@ export default function TimeInput(props: TimeInputProps) { * @description Resets input value to given */ const resetValue = useCallback(() => { - try { - if (typeof time !== 'number' || isNaN(time)) { - throw new Error(`Invalid time value: ${time}`); - } + if (typeof time !== 'number' || isNaN(time)) { + setValue('00:00:00'); + } else { setValue(millisToString(time)); - } catch (error) { - setValue(millisToString(0)); - emitError(`Unable to parse time ${time}: ${error}`); } - }, [emitError, time]); + }, [time]); /** * @description Selects input text on focus @@ -119,9 +113,8 @@ export default function TimeInput(props: TimeInputProps) { ); useEffect(() => { - if (time == null) return; resetValue(); - }, [resetValue, time]); + }, [resetValue]); return ( { options: { value: NonNullable; label: string; + disabled?: boolean; // exposed to allow creating a non-selectable option }[]; placeholder?: string; value?: T; diff --git a/apps/client/src/common/components/switch/Switch.module.scss b/apps/client/src/common/components/switch/Switch.module.scss new file mode 100644 index 000000000..a13dcc8c5 --- /dev/null +++ b/apps/client/src/common/components/switch/Switch.module.scss @@ -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; + } +} diff --git a/apps/client/src/common/components/switch/Switch.tsx b/apps/client/src/common/components/switch/Switch.tsx new file mode 100644 index 000000000..73fb1cee5 --- /dev/null +++ b/apps/client/src/common/components/switch/Switch.tsx @@ -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 ( + + + + ); +} diff --git a/apps/client/src/common/hooks/useEntryAction.ts b/apps/client/src/common/hooks/useEntryAction.ts index 1cd19529f..06e7a18a2 100644 --- a/apps/client/src/common/hooks/useEntryAction.ts +++ b/apps/client/src/common/hooks/useEntryAction.ts @@ -790,7 +790,7 @@ function optimisticDeleteEntries(entryIds: EntryId[], rundown: Rundown) { order = order.filter((id) => id !== entry.id); } else { const parent = entries[entry.parent] as OntimeBlock; - parent.events = parent.events.filter((event) => event !== entry.id); + parent.entries = parent.entries.filter((parentEntry) => parentEntry !== entry.id); } delete entries[entry.id]; diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx index 8e484bd78..dba6362b0 100644 --- a/apps/client/src/features/operator/Operator.tsx +++ b/apps/client/src/features/operator/Operator.tsx @@ -171,21 +171,21 @@ export default function Operator() { return ( - {entry.events.map((nestedEventId) => { - const nestedEvent = data.entries[nestedEventId]; - if (!isOntimeEvent(nestedEvent)) { + {entry.entries.map((nestedEntryId) => { + const nestedEntry = data.entries[nestedEntryId]; + if (!isOntimeEvent(nestedEntry)) { return null; } - const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEvent); + const { isPast, isSelected, isLinkedToLoaded, totalGap } = process(nestedEntry); // hide past events (if setting) and skipped events - if ((hidePast && isPast) || entry.skip) { + if (hidePast && isPast) { return null; } const { mainField, secondaryField, subscribedData } = getEventData( - nestedEvent, + nestedEntry, mainSource, secondarySource, subscribe, @@ -194,16 +194,16 @@ export default function Operator() { return ( )} {isOntimeBlock(entry) ? ( - ); } else if (isOntimeDelay(data)) { - return ; + return ; } return null; } diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index aa50a229e..d35b1e1bc 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -10,7 +10,7 @@ import { useAppMode } from '../../common/stores/appModeStore'; import { handleLinks } from '../../common/utils/linkUtils'; import { cx } from '../../common/utils/styleUtils'; -import RundownEventEditor from './event-editor/RundownEventEditor'; +import RundownEntryEditor from './entry-editor/RundownEntryEditor'; import FinderPlacement from './placements/FinderPlacement'; import RundownWrapper from './RundownWrapper'; @@ -63,7 +63,7 @@ function RundownExport() { {!hideSideBar && (
- +
)} diff --git a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts index d94ddff85..eec21b602 100644 --- a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts +++ b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts @@ -21,7 +21,7 @@ describe('makeRundownMetadata()', () => { block: { id: 'block', type: SupportedEntry.Block, - events: ['11', 'delay', '12', '13'], + entries: ['11', 'delay', '12', '13'], colour: 'red', } as OntimeBlock, '11': { @@ -209,7 +209,7 @@ describe('makeRundownMetadata()', () => { id: 'block', type: SupportedEntry.Block, colour: 'red', - events: ['1', '2'], + entries: ['1', '2'], } as OntimeBlock, '1': { id: '1', @@ -288,12 +288,12 @@ describe('makeSortableList()', () => { it('generates a list with block ends', () => { const order = ['block-1', '2', 'block-3', 'block-4']; const entries: RundownEntries = { - 'block-1': { type: SupportedEntry.Block, id: 'block-1', events: ['11'] } as OntimeBlock, + 'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: ['11'] } as OntimeBlock, '11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent, '2': { type: SupportedEntry.Event, id: '2', parent: null } as OntimeEvent, - 'block-3': { type: SupportedEntry.Block, id: 'block-3', events: ['31'] } as OntimeBlock, + 'block-3': { type: SupportedEntry.Block, id: 'block-3', entries: ['31'] } as OntimeBlock, '31': { type: SupportedEntry.Event, id: '31', parent: 'block-3' } as OntimeEvent, - 'block-4': { type: SupportedEntry.Block, id: 'block-4', events: [] as string[] } as OntimeBlock, + 'block-4': { type: SupportedEntry.Block, id: 'block-4', entries: [] as string[] } as OntimeBlock, }; const sortableList = makeSortableList(order, entries); @@ -313,7 +313,7 @@ describe('makeSortableList()', () => { it('closes dangling blocks', () => { const order = ['block']; const entries: RundownEntries = { - block: { type: SupportedEntry.Block, id: 'block-1', events: ['11', '12'] } as OntimeBlock, + block: { type: SupportedEntry.Block, id: 'block-1', entries: ['11', '12'] } as OntimeBlock, '11': { type: SupportedEntry.Event, id: '11', parent: 'block-1' } as OntimeEvent, '12': { type: SupportedEntry.Event, id: '12', parent: 'block-1' } as OntimeEvent, }; @@ -325,8 +325,8 @@ describe('makeSortableList()', () => { it('handles a list with a with just blocks', () => { const order = ['block-1', 'block-2']; const entries: RundownEntries = { - 'block-1': { type: SupportedEntry.Block, id: 'block-1', events: [] as string[] } as OntimeBlock, - 'block-2': { type: SupportedEntry.Block, id: 'block-2', events: [] as string[] } as OntimeBlock, + 'block-1': { type: SupportedEntry.Block, id: 'block-1', entries: [] as string[] } as OntimeBlock, + 'block-2': { type: SupportedEntry.Block, id: 'block-2', entries: [] as string[] } as OntimeBlock, }; const sortableList = makeSortableList(order, entries); @@ -338,62 +338,62 @@ describe('moveUp()', () => { const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3']; const entries = { event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent, - event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent, - block1: { type: 'block', id: 'block1', events: ['event3'] } as OntimeBlock, + event2: { type: 'event', id: 'event2', parent: null } as OntimeEvent, + block1: { type: 'block', id: 'block1', entries: ['event3'] } as OntimeBlock, event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent, - block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock, + block2: { type: 'block', id: 'block2', entries: [] as EntryId[] } as OntimeBlock, event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent, }; it('moves an event up in the list', () => { const result = moveUp('event2', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'event1', order: 'before', isBlock: false }); - }) + }); it.todo('disallows nesting blocks', () => { const result = moveUp('block2', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'block1', order: 'before', isBlock: false }); - }) + }); it('moves an event into a block', () => { const result = moveUp('event3', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'block2', order: 'insert', isBlock: true }); - }) + }); it('moving up from top is noop', () => { const result = moveUp('event1', sortableData, entries); expect(result).toMatchObject({ destinationId: null }); - }) + }); }); describe('moveDown()', () => { const sortableData = ['event1', 'event2', 'block1', 'event11', 'end-block1', 'block2', 'end-block2', 'event3']; const entries = { event1: { type: 'event', id: 'event1', parent: null } as OntimeEvent, - event2: { type: 'event', id: 'event2', parent: null }as OntimeEvent, - block1: { type: 'block', id: 'block1', events: ['event11'] } as OntimeBlock, + event2: { type: 'event', id: 'event2', parent: null } as OntimeEvent, + block1: { type: 'block', id: 'block1', entries: ['event11'] } as OntimeBlock, event11: { type: 'event', id: 'event11', parent: 'block1' } as OntimeEvent, - block2: { type: 'block', id: 'block2', events: [] as EntryId[] } as OntimeBlock, + block2: { type: 'block', id: 'block2', entries: [] as EntryId[] } as OntimeBlock, event3: { type: 'event', id: 'event3', parent: null } as OntimeEvent, }; it('moves an event down in the list', () => { const result = moveDown('event1', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'event2', order: 'after', isBlock: false }); - }) + }); it.todo('disallows nesting blocks', () => { const result = moveDown('block1', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'block2', order: 'before', isBlock: false }); - }) + }); it('moves an event into a block', () => { const result = moveDown('event2', sortableData, entries); expect(result).toStrictEqual({ destinationId: 'event11', order: 'before', isBlock: true }); - }) + }); it('moving down from bottom is noop', () => { const result = moveDown('event3', sortableData, entries); expect(result).toMatchObject({ destinationId: null }); - }) -}); \ No newline at end of file + }); +}); diff --git a/apps/client/src/features/rundown/entry-editor/BlockEditor.tsx b/apps/client/src/features/rundown/entry-editor/BlockEditor.tsx new file mode 100644 index 000000000..f52158063 --- /dev/null +++ b/apps/client/src/features/rundown/entry-editor/BlockEditor.tsx @@ -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 ( +
+
+ Block schedule +
+
+ { + // TODO: format with user time settings + } + First event start + + {millisToString(block.startTime, { fallback: timerPlaceholder })} + +
+
+ Last event end + + {millisToString(block.endTime, { fallback: timerPlaceholder })} + +
+
+ Scheduled duration + + {millisToString(block.duration, { fallback: enDash })} + +
+
+
+
+ Target duration + +
+
+ Plan offset + { + // TODO: update remote data + // TODO: remove tab index + } + + {millisToString(planOffset, { fallback: enDash })} + +
+
+
+ Is next day? + + { + handleSubmit('isNextDay', checked); + }} + /> + {block.isNextDay ? 'Events start the day after' : '-'} + +
+
+ +
+ Block data +
+ Colour + +
+ +
+ +
+ + Custom Fields + {isEditor && Manage Custom Fields} + + +
+
+ ); +} diff --git a/apps/client/src/features/rundown/event-editor/CuesheetEventEditor.tsx b/apps/client/src/features/rundown/entry-editor/CuesheetEventEditor.tsx similarity index 79% rename from apps/client/src/features/rundown/event-editor/CuesheetEventEditor.tsx rename to apps/client/src/features/rundown/entry-editor/CuesheetEventEditor.tsx index acceaeef9..7b65bebe9 100644 --- a/apps/client/src/features/rundown/event-editor/CuesheetEventEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/CuesheetEventEditor.tsx @@ -6,14 +6,13 @@ import { cx } from '../../../common/utils/styleUtils'; import EventEditor from './EventEditor'; -import style from './EventEditor.module.scss'; +import style from './EntryEditor.module.scss'; interface CuesheetEventEditorProps { eventId: string; } -export default function CuesheetEventEditor(props: CuesheetEventEditorProps) { - const { eventId } = props; +export default function CuesheetEventEditor({ eventId }: CuesheetEventEditorProps) { const { data } = useRundown(); const [event, setEvent] = useState(null); @@ -37,7 +36,7 @@ export default function CuesheetEventEditor(props: CuesheetEventEditorProps) { } return ( -
+
); diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss b/apps/client/src/features/rundown/entry-editor/EntryEditor.module.scss similarity index 82% rename from apps/client/src/features/rundown/event-editor/EventEditor.module.scss rename to apps/client/src/features/rundown/entry-editor/EntryEditor.module.scss index 599bcbcfc..bfc752380 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.module.scss +++ b/apps/client/src/features/rundown/entry-editor/EntryEditor.module.scss @@ -1,4 +1,4 @@ -.eventEditor { +.entryEditor { max-height: 100%; display: flex; flex-direction: column; @@ -88,3 +88,19 @@ grid-template-columns: 1fr 72px; gap: 1rem; } + +/* approximating the style of a disabled input */ +.textLikeInput { + background-color: rgba($gray-1200, 0.4); + font-weight: 400; + color: $gray-200; + border: 1px solid transparent; + + justify-content: center; + + width: 7.5em; + + &:hover { + background-color: rgba($gray-1200, 0.4); + } +} diff --git a/apps/client/src/features/rundown/event-editor/EventEditor.tsx b/apps/client/src/features/rundown/entry-editor/EventEditor.tsx similarity index 74% rename from apps/client/src/features/rundown/event-editor/EventEditor.tsx rename to apps/client/src/features/rundown/entry-editor/EventEditor.tsx index 3b60de05c..0f7f439fb 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/EventEditor.tsx @@ -6,44 +6,38 @@ import AppLink from '../../../common/components/link/app-link/AppLink'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; import useCustomFields from '../../../common/hooks-query/useCustomFields'; -import EventCustom from './composite/EventEditorCustom'; +import EntryEditorCustomFields from './composite/EventEditorCustomFields'; import EventEditorTimes from './composite/EventEditorTimes'; import EventEditorTitles from './composite/EventEditorTitles'; import EventEditorTriggers from './composite/EventEditorTriggers'; -import EventEditorEmpty from './EventEditorEmpty'; -import style from './EventEditor.module.scss'; +import style from './EntryEditor.module.scss'; -// any of the titles + custom field labels -export type EditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | string; +// any of the titles + colour + custom field labels +export type EventEditorUpdateFields = 'cue' | 'title' | 'note' | 'colour' | string; interface EventEditorProps { event: OntimeEvent; } -export default function EventEditor(props: EventEditorProps) { - const { event } = props; +export default function EventEditor({ event }: EventEditorProps) { const { data: customFields } = useCustomFields(); const { updateEntry } = useEntryActions(); const isEditor = window.location.pathname.includes('editor'); const handleSubmit = useCallback( - (field: EditorUpdateFields, value: string) => { + (field: EventEditorUpdateFields, value: string) => { if (field.startsWith('custom-')) { const fieldLabel = field.split('custom-')[1]; - updateEntry({ id: event?.id, custom: { [fieldLabel]: value } }); + updateEntry({ id: event.id, custom: { [fieldLabel]: value } }); } else { - updateEntry({ id: event?.id, [field]: value }); + updateEntry({ id: event.id, [field]: value }); } }, - [event?.id, updateEntry], + [event.id, updateEntry], ); - if (!event) { - return ; - } - return (
Manage Custom Fields} - +
diff --git a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss b/apps/client/src/features/rundown/entry-editor/EventEditorEmpty.module.scss similarity index 69% rename from apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss rename to apps/client/src/features/rundown/entry-editor/EventEditorEmpty.module.scss index 9d8da825c..0c19eda57 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss +++ b/apps/client/src/features/rundown/entry-editor/EventEditorEmpty.module.scss @@ -1,4 +1,4 @@ -.eventEditor { +.entryEditor { color: $label-gray; height: 100%; max-height: 100%; @@ -46,3 +46,15 @@ text-align: center; width: 1em; } + +.kbd { + font-family: monospace; + white-space: nowrap; + font-size: calc(1rem - 2px); + padding: 0.125rem 0.5rem; + background-color: $gray-1200; + color: $ui-white; + border-radius: 2px; + font-weight: 400; + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.4); +} diff --git a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx b/apps/client/src/features/rundown/entry-editor/EventEditorEmpty.tsx similarity index 96% rename from apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx rename to apps/client/src/features/rundown/entry-editor/EventEditorEmpty.tsx index 7a90337d7..ba62e7633 100644 --- a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx +++ b/apps/client/src/features/rundown/entry-editor/EventEditorEmpty.tsx @@ -1,5 +1,4 @@ import { memo, PropsWithChildren } from 'react'; -import { Kbd } from '@chakra-ui/react'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils'; @@ -10,7 +9,7 @@ export default memo(EventEditorEmpty); function EventEditorEmpty() { return ( -
+
Rundown shortcuts @@ -172,3 +171,7 @@ function EventEditorEmpty() { function AuxKey({ children }: PropsWithChildren) { return {children}; } + +function Kbd({ children }: PropsWithChildren) { + return {children}; +} diff --git a/apps/client/src/features/rundown/event-editor/RundownEventEditor.tsx b/apps/client/src/features/rundown/entry-editor/RundownEntryEditor.tsx similarity index 59% rename from apps/client/src/features/rundown/event-editor/RundownEventEditor.tsx rename to apps/client/src/features/rundown/entry-editor/RundownEntryEditor.tsx index 54ccb71a3..d23cffb7d 100644 --- a/apps/client/src/features/rundown/event-editor/RundownEventEditor.tsx +++ b/apps/client/src/features/rundown/entry-editor/RundownEntryEditor.tsx @@ -1,20 +1,21 @@ import { useEffect, useState } from 'react'; -import { isOntimeEvent, OntimeEvent } from 'ontime-types'; +import { isOntimeBlock, isOntimeDelay, OntimeBlock, OntimeEvent } from 'ontime-types'; import useRundown from '../../../common/hooks-query/useRundown'; import { useEventSelection } from '../useEventSelection'; -import { EventEditorFooter } from './composite/EventEditorFooter'; +import EventEditorFooter from './composite/EventEditorFooter'; +import BlockEditor from './BlockEditor'; import EventEditor from './EventEditor'; import EventEditorEmpty from './EventEditorEmpty'; -import style from './EventEditor.module.scss'; +import style from './EntryEditor.module.scss'; -export default function RundownEventEditor() { +export default function RundownEntryEditor() { const selectedEvents = useEventSelection((state) => state.selectedEvents); const { data } = useRundown(); - const [event, setEvent] = useState(null); + const [event, setEvent] = useState(null); useEffect(() => { if (data.order.length === 0) { @@ -29,7 +30,7 @@ export default function RundownEventEditor() { } const event = data.entries[selectedEventId]; - if (event && isOntimeEvent(event)) { + if (event && !isOntimeDelay(event)) { setEvent(event); } else { setEvent(null); @@ -40,8 +41,16 @@ export default function RundownEventEditor() { return ; } + if (isOntimeBlock(event)) { + return ( +
+ +
+ ); + } + return ( -
+
diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorCustom.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorCustomFields.tsx similarity index 77% rename from apps/client/src/features/rundown/event-editor/composite/EventEditorCustom.tsx rename to apps/client/src/features/rundown/entry-editor/composite/EventEditorCustomFields.tsx index 41afa07a4..2577c6450 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorCustom.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorCustomFields.tsx @@ -1,23 +1,26 @@ import { CSSProperties, Fragment } from 'react'; -import { CustomFields, OntimeEvent } from 'ontime-types'; +import { CustomFields, OntimeBlock, OntimeEvent } from 'ontime-types'; import { getAccessibleColour } from '../../../../common/utils/styleUtils'; -import { EditorUpdateFields } from '../EventEditor'; +import { EventEditorUpdateFields } from '../EventEditor'; import EventEditorImage from './EventEditorImage'; import EventTextArea from './EventTextArea'; -import EventTextInput from './EventTextInput'; +import EntryEditorTextInput from './EventTextInput'; -import style from '../EventEditor.module.scss'; +import style from '../EntryEditor.module.scss'; -interface EventEditorCustomProps { +interface EntryEditorCustomFieldsProps { fields: CustomFields; - event: OntimeEvent; - handleSubmit: (field: EditorUpdateFields, value: string) => void; + event: OntimeEvent | OntimeBlock; + handleSubmit: (field: EventEditorUpdateFields, value: string) => void; } -export default function EventEditorCustom(props: EventEditorCustomProps) { - const { fields: customFields, handleSubmit, event } = props; +export default function EntryEditorCustomFields({ + fields: customFields, + handleSubmit, + event, +}: EntryEditorCustomFieldsProps) { return ( {Object.keys(customFields).map((fieldKey) => { @@ -44,7 +47,7 @@ export default function EventEditorCustom(props: EventEditorCustomProps) { if (customFields[fieldKey].type === 'image') { return (
- diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx similarity index 73% rename from apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx rename to apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx index 42f7e8723..0ddf7c3b8 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTimes.tsx @@ -1,16 +1,18 @@ import { memo } from 'react'; import { IoInformationCircle } from 'react-icons/io5'; -import { Select, Switch, Tooltip } from '@chakra-ui/react'; +import { Tooltip } from '@chakra-ui/react'; import { EndAction, TimerType, TimeStrategy } from 'ontime-types'; import { millisToString, parseUserTime } from 'ontime-utils'; import * as Editor from '../../../../common/components/editor-utils/EditorUtils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; +import Select from '../../../../common/components/select/Select'; +import Switch from '../../../../common/components/switch/Switch'; import { useEntryActions } from '../../../../common/hooks/useEntryAction'; import { millisToDelayString } from '../../../../common/utils/dateConfig'; import TimeInputFlow from '../../time-input-flow/TimeInputFlow'; -import style from '../EventEditor.module.scss'; +import style from '../EntryEditor.module.scss'; interface EventEditorTimesProps { eventId: string; @@ -30,26 +32,25 @@ interface EventEditorTimesProps { type HandledActions = 'countToEnd' | 'timerType' | 'endAction' | 'timeWarning' | 'timeDanger'; export default memo(EventEditorTimes); -function EventEditorTimes(props: EventEditorTimesProps) { - const { - eventId, - timeStart, - timeEnd, - duration, - timeStrategy, - linkStart, - countToEnd, - delay, - endAction, - timerType, - timeWarning, - timeDanger, - } = props; +function EventEditorTimes({ + eventId, + timeStart, + timeEnd, + duration, + timeStrategy, + linkStart, + countToEnd, + delay, + endAction, + timerType, + timeWarning, + timeDanger, +}: EventEditorTimesProps) { const { updateEntry } = useEntryActions(); const handleSubmit = (field: HandledActions, value: string | boolean) => { if (field === 'countToEnd') { - updateEntry({ id: eventId, countToEnd: !(value as boolean) }); + updateEntry({ id: eventId, countToEnd: value as boolean }); return; } @@ -100,27 +101,22 @@ function EventEditorTimes(props: EventEditorTimesProps) {
End Action + onChange={(value) => handleSubmit('endAction', value)} + options={[ + { value: EndAction.None, label: 'None' }, + { value: EndAction.LoadNext, label: 'Load next event' }, + { value: EndAction.PlayNext, label: 'Play next event' }, + ]} + />
Count to End handleSubmit('countToEnd', countToEnd)} - variant='ontime' + checked={countToEnd} + onCheckedChange={(value) => handleSubmit('countToEnd', value)} /> {countToEnd ? 'On' : 'Off'} @@ -141,18 +137,15 @@ function EventEditorTimes(props: EventEditorTimesProps) {
Timer Type + onChange={(value) => handleSubmit('timerType', value)} + options={[ + { value: TimerType.CountDown, label: 'Count down' }, + { value: TimerType.CountUp, label: 'Count up' }, + { value: TimerType.Clock, label: 'Clock' }, + { value: TimerType.None, label: 'None' }, + ]} + />
diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx similarity index 57% rename from apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx rename to apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx index cfeb48834..5aee75ae1 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTitles.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTitles.tsx @@ -1,15 +1,15 @@ import { memo } from 'react'; -import { Input } from '@chakra-ui/react'; import { sanitiseCue } from 'ontime-utils'; import * as Editor from '../../../../common/components/editor-utils/EditorUtils'; import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect'; -import { type EditorUpdateFields } from '../EventEditor'; +import Input from '../../../../common/components/input/input/Input'; +import { type EventEditorUpdateFields } from '../EventEditor'; import EventTextArea from './EventTextArea'; -import EventTextInput from './EventTextInput'; +import EntryEditorTextInput from './EventTextInput'; -import style from '../EventEditor.module.scss'; +import style from '../EntryEditor.module.scss'; interface EventEditorTitlesProps { eventId: string; @@ -17,12 +17,11 @@ interface EventEditorTitlesProps { title: string; note: string; colour: string; - handleSubmit: (field: EditorUpdateFields, value: string) => void; + handleSubmit: (field: EventEditorUpdateFields, value: string) => void; } -const EventEditorTitles = (props: EventEditorTitlesProps) => { - const { eventId, cue, title, note, colour, handleSubmit } = props; - +export default memo(EventEditorTitles); +function EventEditorTitles({ eventId, cue, title, note, colour, handleSubmit }: EventEditorTitlesProps) { const cueSubmitHandler = (_field: string, newValue: string) => { handleSubmit('cue', sanitiseCue(newValue)); }; @@ -33,25 +32,22 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
Event ID (read only) - +
- +
Colour
- +
); -}; - -export default memo(EventEditorTitles); +} diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTriggers.module.scss b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.module.scss similarity index 100% rename from apps/client/src/features/rundown/event-editor/composite/EventEditorTriggers.module.scss rename to apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.module.scss diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTriggers.tsx b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx similarity index 73% rename from apps/client/src/features/rundown/event-editor/composite/EventEditorTriggers.tsx rename to apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx index b307af4a1..8ab02426f 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTriggers.tsx +++ b/apps/client/src/features/rundown/entry-editor/composite/EventEditorTriggers.tsx @@ -1,9 +1,12 @@ import { Fragment, useCallback, useState } from 'react'; import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5'; -import { Button, IconButton, Select, Tooltip } from '@chakra-ui/react'; +import { Tooltip } from '@chakra-ui/react'; import { TimerLifeCycle, timerLifecycleValues, Trigger } from 'ontime-types'; import { generateId } from 'ontime-utils'; +import Button from '../../../../common/components/buttons/Button'; +import IconButton from '../../../../common/components/buttons/IconButton'; +import Select from '../../../../common/components/select/Select'; import Tag from '../../../../common/components/tag/Tag'; import { useEntryActions } from '../../../../common/hooks/useEntryAction'; import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; @@ -17,8 +20,7 @@ interface EventEditorTriggersProps { triggers: Trigger[]; } -export default function EventEditorTriggers(props: EventEditorTriggersProps) { - const { triggers, eventId } = props; +export default function EventEditorTriggers({ triggers, eventId }: EventEditorTriggersProps) { const showTriggers = triggers.length > 0; return ( @@ -34,8 +36,7 @@ interface EventTriggerFormProps { triggers?: Trigger[]; } -function EventTriggerForm(props: EventTriggerFormProps) { - const { eventId, triggers } = props; +function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) { const { data: automationSettings } = useAutomationSettings(); const { updateEntry } = useEntryActions(); const [automationId, setAutomationId] = useState(undefined); @@ -68,38 +69,21 @@ function EventTriggerForm(props: EventTriggerFormProps) { return (
+ placeholder='Choose a trigger' + onChange={(value) => setCycleValue(value)} + options={eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle }))} + /> + + placeholder='Choose an automation' + onChange={(value) => setAutomationId(value)} + options={Object.values(automationSettings.automations).map(({ id, title }) => ({ value: id, label: title }))} + /> +
); diff --git a/apps/client/src/features/rundown/event-editor/composite/eventTrigger.constants.ts b/apps/client/src/features/rundown/entry-editor/composite/eventTrigger.constants.ts similarity index 100% rename from apps/client/src/features/rundown/event-editor/composite/eventTrigger.constants.ts rename to apps/client/src/features/rundown/entry-editor/composite/eventTrigger.constants.ts diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss index 8a70a88f2..bb7ccb63f 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss @@ -5,5 +5,5 @@ padding-block: 0.5rem; padding-left: calc(2em + 0.5rem); - background-color: color-mix(in srgb, var(--user-bg, transparent) 10%, transparent 90%); + background-color: color-mix(in srgb, var(--user-bg, transparent) 15%, transparent 85%); } diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index a71b877ed..85eb32dfe 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -14,9 +14,7 @@ interface QuickAddBlockProps { } export default memo(QuickAddBlock); - -function QuickAddBlock(props: QuickAddBlockProps) { - const { previousEventId, parentBlock, backgroundColor } = props; +function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickAddBlockProps) { const { addEntry } = useEntryActions(); const doLinkPrevious = useRef(null); diff --git a/apps/client/src/features/rundown/block-block/BlockEnd.module.scss b/apps/client/src/features/rundown/rundown-block/BlockEnd.module.scss similarity index 91% rename from apps/client/src/features/rundown/block-block/BlockEnd.module.scss rename to apps/client/src/features/rundown/rundown-block/BlockEnd.module.scss index 93f06d606..fcef9bf25 100644 --- a/apps/client/src/features/rundown/block-block/BlockEnd.module.scss +++ b/apps/client/src/features/rundown/rundown-block/BlockEnd.module.scss @@ -2,7 +2,7 @@ .blockEnd { cursor: default; - height: 0.5rem; + height: 1rem; background-color: var(--user-bg, $gray-1050); border-radius: 0 0 $block-border-radius $block-border-radius; diff --git a/apps/client/src/features/rundown/block-block/BlockEnd.tsx b/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx similarity index 89% rename from apps/client/src/features/rundown/block-block/BlockEnd.tsx rename to apps/client/src/features/rundown/rundown-block/BlockEnd.tsx index a357da421..b3bd9ebfb 100644 --- a/apps/client/src/features/rundown/block-block/BlockEnd.tsx +++ b/apps/client/src/features/rundown/rundown-block/BlockEnd.tsx @@ -8,8 +8,7 @@ interface BlockEndProps { colour?: string; } -export default function BlockEnd(props: BlockEndProps) { - const { id, colour } = props; +export default function BlockEnd({ id, colour }: BlockEndProps) { const { attributes: dragAttributes, listeners: dragListeners, @@ -37,6 +36,7 @@ export default function BlockEnd(props: BlockEndProps) { ...dragStyle, ...(colour ? { '--user-bg': colour } : {}), }} + tabIndex={-1} /> ); } diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.module.scss b/apps/client/src/features/rundown/rundown-block/RundownBlock.module.scss similarity index 90% rename from apps/client/src/features/rundown/block-block/BlockBlock.module.scss rename to apps/client/src/features/rundown/rundown-block/RundownBlock.module.scss index df2a63b13..a9ad86a5d 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.module.scss +++ b/apps/client/src/features/rundown/rundown-block/RundownBlock.module.scss @@ -26,9 +26,11 @@ color: $section-white; font-size: 1rem; display: grid; - justify-content: center; - padding-top: 0.25rem; - } + place-content: center; + position: relative; + cursor: pointer; + } + .header { grid-area: header; @@ -63,6 +65,8 @@ .drag { @include drag-style; + position: absolute; + margin-top: 0.25rem; &.isDragging { cursor: grabbing; diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.tsx b/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx similarity index 81% rename from apps/client/src/features/rundown/block-block/BlockBlock.tsx rename to apps/client/src/features/rundown/rundown-block/RundownBlock.tsx index 7690cdef8..bd4874afc 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.tsx +++ b/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react'; +import { MouseEvent, useRef } from 'react'; import { IoChevronDown, IoChevronUp, @@ -18,20 +18,21 @@ import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../../common/utils/time'; import EditableBlockTitle from '../common/EditableBlockTitle'; import { canDrop } from '../rundown.utils'; +import { useEventSelection } from '../useEventSelection'; -import style from './BlockBlock.module.scss'; +import style from './RundownBlock.module.scss'; -interface BlockBlockProps { +interface RundownBlockProps { data: OntimeBlock; hasCursor: boolean; collapsed: boolean; onCollapse: (collapsed: boolean, groupId: EntryId) => void; } -export default function BlockBlock(props: BlockBlockProps) { - const { data, hasCursor, collapsed, onCollapse } = props; +export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: RundownBlockProps) { const handleRef = useRef(null); const { clone, ungroup, deleteEntry } = useEntryActions(); + const { selectedEvents, setSelectedBlock } = useEventSelection(); const [onContextMenu] = useContextMenu([ { @@ -43,7 +44,7 @@ export default function BlockBlock(props: BlockBlockProps) { label: 'Ungroup', icon: IoFolderOpenOutline, onClick: () => ungroup(data.id), - isDisabled: data.events.length === 0, + isDisabled: data.entries.length === 0, }, { label: 'Delete Block', @@ -70,6 +71,20 @@ export default function BlockBlock(props: BlockBlockProps) { animateLayoutChanges: () => false, }); + const handleFocusClick = (event: MouseEvent) => { + event.stopPropagation(); + + // event.button === 2 is a right-click + // disable selection if the user selected events and right clicks + // so the context menu shows up + if (selectedEvents.size > 1 && event.button === 2) { + return; + } + + // UI indexes are 1 based + setSelectedBlock({ id: data.id }); + }; + const binderColours = data.colour && getAccessibleColour(data.colour); const isValidDrop = over?.id && canDrop(over.data.current?.type, over.data.current?.parent); @@ -84,6 +99,7 @@ export default function BlockBlock(props: BlockBlockProps) {
Events
-
{data.events.length}
+
{data.entries.length}
diff --git a/apps/client/src/features/rundown/delay-block/DelayBlock.module.scss b/apps/client/src/features/rundown/rundown-delay/RundownDelay.module.scss similarity index 100% rename from apps/client/src/features/rundown/delay-block/DelayBlock.module.scss rename to apps/client/src/features/rundown/rundown-delay/RundownDelay.module.scss diff --git a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx similarity index 92% rename from apps/client/src/features/rundown/delay-block/DelayBlock.tsx rename to apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx index 53a9ceeaf..e216f6568 100644 --- a/apps/client/src/features/rundown/delay-block/DelayBlock.tsx +++ b/apps/client/src/features/rundown/rundown-delay/RundownDelay.tsx @@ -9,15 +9,14 @@ import DelayInput from '../../../common/components/input/delay-input/DelayInput' import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { cx } from '../../../common/utils/styleUtils'; -import style from './DelayBlock.module.scss'; +import style from './RundownDelay.module.scss'; -interface DelayBlockProps { +interface RundownDelayProps { data: OntimeDelay; hasCursor: boolean; } -export default function DelayBlock(props: DelayBlockProps) { - const { data, hasCursor } = props; +export default function RundownDelay({ data, hasCursor }: RundownDelayProps) { const { applyDelay, deleteEntry } = useEntryActions(); const handleRef = useRef(null); diff --git a/apps/client/src/features/rundown/event-block/EventBlock.module.scss b/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss similarity index 99% rename from apps/client/src/features/rundown/event-block/EventBlock.module.scss rename to apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss index b83a9c5bb..225c3478c 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.module.scss +++ b/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss @@ -2,7 +2,7 @@ $skip-opacity: 0.2; -.eventBlock { +.rundownEvent { @include block-styling; background-color: $block-bg; margin-block: 0.25rem; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx similarity index 91% rename from apps/client/src/features/rundown/event-block/EventBlock.tsx rename to apps/client/src/features/rundown/rundown-event/RundownEvent.tsx index 771dba43d..8680406ed 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/rundown-event/RundownEvent.tsx @@ -19,12 +19,12 @@ import type { EventItemActions } from '../RundownEntry'; import { useEventIdSwapping } from '../useEventIdSwapping'; import { getSelectionMode, useEventSelection } from '../useEventSelection'; -import EventBlockInner from './EventBlockInner'; +import RundownEventInner from './RundownEventInner'; import RundownIndicators from './RundownIndicators'; -import style from './EventBlock.module.scss'; +import style from './RundownEvent.module.scss'; -interface EventBlockProps { +interface RundownEventProps { eventId: EntryId; cue: string; timeStart: number; @@ -65,39 +65,38 @@ interface EventBlockProps { hasTriggers: boolean; } -export default function EventBlock(props: EventBlockProps) { - const { - eventId, - cue, - timeStart, - timeEnd, - duration, - timeStrategy, - linkStart, - countToEnd, - eventIndex, - endAction, - timerType, - title, - note, - delay, - colour, - isPast, - isNext, - skip = false, - parent, - loaded, - hasCursor, - playback, - isRolling, - gap, - isNextDay, - dayOffset, - totalGap, - isLinkedToLoaded, - actionHandler, - hasTriggers, - } = props; +export default function RundownEvent({ + eventId, + cue, + timeStart, + timeEnd, + duration, + timeStrategy, + linkStart, + countToEnd, + eventIndex, + endAction, + timerType, + title, + note, + delay, + colour, + isPast, + isNext, + skip = false, + parent, + loaded, + hasCursor, + playback, + isRolling, + gap, + isNextDay, + dayOffset, + totalGap, + isLinkedToLoaded, + actionHandler, + hasTriggers, +}: RundownEventProps) { const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping(); const { selectedEvents, setSelectedEvents } = useEventSelection(); const handleRef = useRef(null); @@ -224,7 +223,7 @@ export default function EventBlock(props: EventBlockProps) { const isSelected = selectedEvents.has(eventId); const blockClasses = cx([ - style.eventBlock, + style.rundownEvent, skip ? style.skip : null, isPast ? style.past : null, loaded ? style.loaded : null, @@ -268,7 +267,7 @@ export default function EventBlock(props: EventBlockProps) {
{isVisible && ( - { @@ -119,7 +118,7 @@ function EventBlockInner(props: EventBlockInnerProps) { disablePlayback={skip || isRolling} /> {!skip && ( - { it('adds a given delay to the start time', () => { diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.module.scss similarity index 100% rename from apps/client/src/features/rundown/event-block/composite/EventBlockChip.module.scss rename to apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.module.scss diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx similarity index 93% rename from apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx rename to apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx index f34495f05..772ac593f 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockChip.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx @@ -9,9 +9,9 @@ import { cx } from '../../../../common/utils/styleUtils'; import { formatDuration, formatTime, useTimeUntilStart } from '../../../../common/utils/time'; import { tooltipDelayFast } from '../../../../ontimeConfig'; -import style from './EventBlockChip.module.scss'; +import style from './RundownEventChip.module.scss'; -interface EventBlockChipProps { +interface RundownEventChipProps { id: string; timeStart: number; delay: number; @@ -24,8 +24,18 @@ interface EventBlockChipProps { isLinkedToLoaded: boolean; } -export default function EventBlockChip(props: EventBlockChipProps) { - const { timeStart, delay, dayOffset, isPast, isLoaded, className, totalGap, id, duration, isLinkedToLoaded } = props; +export default function RundownEventChip({ + timeStart, + delay, + dayOffset, + isPast, + isLoaded, + className, + totalGap, + id, + duration, + isLinkedToLoaded, +}: RundownEventChipProps) { const { playback } = usePlayback(); if (isLoaded) { diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx similarity index 92% rename from apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx rename to apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx index 8caf9f07c..3d55841c2 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockPlayback.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventPlayback.tsx @@ -6,7 +6,7 @@ import { useEntryActions } from '../../../../common/hooks/useEntryAction'; import { setEventPlayback } from '../../../../common/hooks/useSocket'; import { tooltipDelayMid } from '../../../../ontimeConfig'; -import style from '../EventBlock.module.scss'; +import style from '../RundownEvent.module.scss'; const blockBtnStyle = { size: 'sm', @@ -23,7 +23,7 @@ const tooltipProps = { openDelay: tooltipDelayMid, }; -interface EventBlockPlaybackProps { +interface RundownEventPlaybackProps { eventId: string; skip: boolean; isPlaying: boolean; @@ -32,8 +32,15 @@ interface EventBlockPlaybackProps { disablePlayback: boolean; } -const EventBlockPlayback = (props: EventBlockPlaybackProps) => { - const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props; +export default memo(RundownEventPlayback); +function RundownEventPlayback({ + eventId, + skip, + isPlaying, + isPaused, + loaded, + disablePlayback, +}: RundownEventPlaybackProps) { const { updateEntry } = useEntryActions(); const toggleSkip = (event: MouseEvent) => { @@ -123,6 +130,4 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => { />
); -}; - -export default memo(EventBlockPlayback); +} diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.module.scss b/apps/client/src/features/rundown/rundown-event/composite/RundownEventProgressBar.module.scss similarity index 100% rename from apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.module.scss rename to apps/client/src/features/rundown/rundown-event/composite/RundownEventProgressBar.module.scss diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventProgressBar.tsx similarity index 73% rename from apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx rename to apps/client/src/features/rundown/rundown-event/composite/RundownEventProgressBar.tsx index 40849ec89..68c356d81 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventProgressBar.tsx @@ -1,9 +1,9 @@ import { useTimer } from '../../../../common/hooks/useSocket'; import { getProgress } from '../../../../common/utils/getProgress'; -import style from './EventBlockProgressBar.module.scss'; +import style from './RundownEventProgressBar.module.scss'; -export default function EventBlockProgressBar() { +export default function RundownEventProgressBar() { const timer = useTimer(); const progress = getProgress(timer.current, timer.duration); diff --git a/apps/client/src/features/rundown/event-block/EventBlock.utils.ts b/apps/client/src/features/rundown/rundown-event/rundownEvent.utils.ts similarity index 100% rename from apps/client/src/features/rundown/event-block/EventBlock.utils.ts rename to apps/client/src/features/rundown/rundown-event/rundownEvent.utils.ts diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index 58159c6d1..75e83883b 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -140,7 +140,7 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent // inside a block there are delays and events // there is no need for special handling flatIds.push(entry.id); - flatIds.push(...entry.events); + flatIds.push(...entry.entries); // close the block flatIds.push(`end-${entry.id}`); @@ -225,7 +225,7 @@ export function moveDown(entryId: EntryId, sortableData: EntryId[], entries: Run return { destinationId: nextEntryId, order: 'after', isBlock: false }; } - const firstBlockChild = entries[nextEntryId].events.at(0); + const firstBlockChild = entries[nextEntryId].entries.at(0); if (firstBlockChild) { // 2. add before the first child of the block return { destinationId: firstBlockChild, order: 'before', isBlock: true }; diff --git a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx index c1fcf0606..f26796133 100644 --- a/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx +++ b/apps/client/src/features/rundown/time-input-flow/TimeInputFlow.tsx @@ -24,6 +24,7 @@ interface EventBlockTimerProps { showLabels?: boolean; } +export default memo(TimeInputFlow); function TimeInputFlow(props: EventBlockTimerProps) { const { eventId, countToEnd, timeStart, timeEnd, duration, timeStrategy, linkStart, delay, showLabels } = props; const { updateEntry, updateTimer } = useEntryActions(); @@ -135,5 +136,3 @@ function TimeInputFlow(props: EventBlockTimerProps) { ); } - -export default memo(TimeInputFlow); diff --git a/apps/client/src/features/rundown/useEventSelection.ts b/apps/client/src/features/rundown/useEventSelection.ts index f1591a7a4..09a059875 100644 --- a/apps/client/src/features/rundown/useEventSelection.ts +++ b/apps/client/src/features/rundown/useEventSelection.ts @@ -12,6 +12,8 @@ interface EventSelectionStore { selectedEvents: Set; anchoredIndex: MaybeNumber; cursor: MaybeString; + entryMode: 'event' | 'block' | null; + setSelectedBlock: (selectionArgs: { id: EntryId }) => void; setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void; clearSelectedEvents: () => void; clearMultiSelect: () => void; @@ -22,13 +24,21 @@ export const useEventSelection = create()((set, get) => ({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, - setSelectedEvents: (selectionArgs) => { - const { id, index, selectMode } = selectionArgs; - const { selectedEvents, anchoredIndex } = get(); + entryMode: null, + setSelectedBlock: ({ id }) => { + set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'block' }); + }, + setSelectedEvents: ({ id, index, selectMode }) => { + const { selectedEvents, anchoredIndex, entryMode } = get(); + + // if we are in block mode, we replace the selection and change the mode + if (entryMode === 'block') { + return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' }); + } // on click, we replace selection with event if (selectMode === 'click') { - return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id }); + return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' }); } // on ctrl + click, we toggle the selection of that event @@ -42,6 +52,7 @@ export const useEventSelection = create()((set, get) => ({ selectedEvents: selectedEvents.add(id), anchoredIndex: index, cursor: id, + entryMode: 'event', }); } @@ -57,6 +68,7 @@ export const useEventSelection = create()((set, get) => ({ return set({ selectedEvents, anchoredIndex: nextIndex < 0 ? rundownData.order.length - 1 : nextIndex, + entryMode: 'event', }); } @@ -83,19 +95,23 @@ export const useEventSelection = create()((set, get) => ({ return set({ selectedEvents: new Set([...selectedEvents, ...selectedEventIds]), anchoredIndex: index, + entryMode: 'event', }); } }, - clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null }), + clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }), clearMultiSelect: () => { const { selectedEvents } = get(); const [firstSelected] = selectedEvents; - set({ selectedEvents: new Set(firstSelected || undefined), anchoredIndex: null }); + set({ selectedEvents: new Set(firstSelected || undefined), anchoredIndex: null, entryMode: null }); }, unselect: (id: string) => { - const { selectedEvents } = get(); + const { entryMode, selectedEvents } = get(); selectedEvents.delete(id); - set({ selectedEvents }); + set({ + selectedEvents, + entryMode: selectedEvents.size === 0 ? null : entryMode, + }); }, })); diff --git a/apps/client/src/views/cuesheet/CuesheetPage.tsx b/apps/client/src/views/cuesheet/CuesheetPage.tsx index 00b7a62b8..654e5f806 100644 --- a/apps/client/src/views/cuesheet/CuesheetPage.tsx +++ b/apps/client/src/views/cuesheet/CuesheetPage.tsx @@ -10,7 +10,7 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import useCustomFields from '../../common/hooks-query/useCustomFields'; import { useFlatRundown } from '../../common/hooks-query/useRundown'; import { CuesheetOverview } from '../../features/overview/Overview'; -import CuesheetEventEditor from '../../features/rundown/event-editor/CuesheetEventEditor'; +import CuesheetEventEditor from '../../features/rundown/entry-editor/CuesheetEventEditor'; import CuesheetDnd from './cuesheet-dnd/CuesheetDnd'; import CuesheetProgress from './cuesheet-progress/CuesheetProgress'; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss index e2f98ce7a..c37a990af 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss @@ -1,12 +1,15 @@ -/* element attempts matching input styles */ +/* element matching input styles */ .textInput { height: 2rem; background-color: transparent; - border-radius: 3px; + border-radius: $component-border-radius-md; display: flex; align-items: center; gap: 0.25rem; + letter-spacing: 1px; + font-size: 1rem; + font-variant-numeric: tabular-nums; &.delayed { color: $ontime-delay-text; diff --git a/apps/server/src/api-data/rundown/__mocks__/rundown.mocks.ts b/apps/server/src/api-data/rundown/__mocks__/rundown.mocks.ts index 6530aba0c..b90557773 100644 --- a/apps/server/src/api-data/rundown/__mocks__/rundown.mocks.ts +++ b/apps/server/src/api-data/rundown/__mocks__/rundown.mocks.ts @@ -10,7 +10,7 @@ const baseEvent = { const baseBlock = { type: SupportedEntry.Block, - events: [], + entries: [], }; /** diff --git a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts index b95c0e0aa..096e00847 100644 --- a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts +++ b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts @@ -570,7 +570,7 @@ describe('processRundown()', () => { const rundown = makeRundown({ order: ['1'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['100', '200', '300'] }), + '1': makeOntimeBlock({ id: '1', entries: ['100', '200', '300'] }), '100': makeOntimeEvent({ id: '100', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }), '200': makeOntimeEvent({ id: '200', timeStart: 200, timeEnd: 300, duration: 100 }), '300': makeOntimeEvent({ id: '300', timeStart: 300, timeEnd: 400, duration: 100 }), @@ -584,7 +584,7 @@ describe('processRundown()', () => { expect(generatedRundown.entries).toMatchObject({ '1': { type: SupportedEntry.Block, - events: ['100', '200', '300'], + entries: ['100', '200', '300'], startTime: 100, endTime: 400, duration: 300, @@ -601,15 +601,15 @@ describe('processRundown()', () => { order: ['0', '1', '2', '3'], entries: { '0': makeOntimeEvent({ id: '0', timeStart: 0, timeEnd: 10, duration: 10, linkStart: false }), - '1': makeOntimeBlock({ id: '1', events: ['101', '102', '103'] }), + '1': makeOntimeBlock({ id: '1', entries: ['101', '102', '103'] }), '101': makeOntimeEvent({ id: '101', timeStart: 100, timeEnd: 200, duration: 100, linkStart: false }), '102': makeOntimeEvent({ id: '102', timeStart: 200, timeEnd: 300, duration: 100, linkStart: true }), '103': makeOntimeEvent({ id: '103', timeStart: 300, timeEnd: 400, duration: 100, linkStart: true }), - '2': makeOntimeBlock({ id: '2', events: ['201', '202', '203'] }), + '2': makeOntimeBlock({ id: '2', entries: ['201', '202', '203'] }), '201': makeOntimeEvent({ id: '201', timeStart: 500, timeEnd: 600, duration: 100, linkStart: false }), '202': makeOntimeEvent({ id: '202', timeStart: 600, timeEnd: 700, duration: 100, linkStart: true }), '203': makeOntimeEvent({ id: '203', timeStart: 700, timeEnd: 800, duration: 100, linkStart: true }), - '3': makeOntimeBlock({ id: '3', events: ['301', '302', '303'] }), + '3': makeOntimeBlock({ id: '3', entries: ['301', '302', '303'] }), '301': makeOntimeEvent({ id: '301', timeStart: 900, timeEnd: 1000, duration: 100, linkStart: false }), '302': makeOntimeEvent({ id: '302', timeStart: 1000, timeEnd: 1100, duration: 100, linkStart: true }), '303': makeOntimeEvent({ id: '303', timeStart: 1100, timeEnd: 1200, duration: 100, linkStart: true }), @@ -624,7 +624,7 @@ describe('processRundown()', () => { '0': { type: SupportedEntry.Event, parent: null }, '1': { type: SupportedEntry.Block, - events: ['101', '102', '103'], + entries: ['101', '102', '103'], startTime: 100, endTime: 400, duration: 300, @@ -635,7 +635,7 @@ describe('processRundown()', () => { '103': { parent: '1' }, '2': { type: SupportedEntry.Block, - events: ['201', '202', '203'], + entries: ['201', '202', '203'], startTime: 500, endTime: 800, duration: 300, @@ -646,7 +646,7 @@ describe('processRundown()', () => { '203': { id: '203', timeStart: 700, timeEnd: 800, duration: 100 }, '3': { type: SupportedEntry.Block, - events: ['301', '302', '303'], + entries: ['301', '302', '303'], startTime: 900, endTime: 1200, duration: 300, @@ -789,7 +789,7 @@ describe('rundownMutation.remove()', () => { const rundown = makeRundown({ order: ['1', '4'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['2', '3'] }), + '1': makeOntimeBlock({ id: '1', entries: ['2', '3'] }), '2': makeOntimeEvent({ id: '2', parent: '1' }), '3': makeOntimeDelay({ id: '3', parent: '1' }), '4': makeOntimeEvent({ id: '4', parent: null }), @@ -811,7 +811,7 @@ describe('rundownMutation.remove()', () => { const rundown = makeRundown({ order: ['1', '4'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['2', '3'] }), + '1': makeOntimeBlock({ id: '1', entries: ['2', '3'] }), '2': makeOntimeEvent({ id: '2', parent: '1' }), '3': makeOntimeDelay({ id: '3', parent: '1' }), '4': makeOntimeEvent({ id: '4', parent: null }), @@ -823,7 +823,7 @@ describe('rundownMutation.remove()', () => { expect(rundown.order).toStrictEqual(['1', '4']); expect(rundown.entries).not.toHaveProperty('2'); expect(rundown.entries['1']).toMatchObject({ - events: ['3'], + entries: ['3'], }); }); }); @@ -853,7 +853,7 @@ describe('rundownMutation.reorder()', () => { const rundown = makeRundown({ order: ['1', '2', '3'], entries: { - '1': makeOntimeBlock({ id: '1', events: [] }), + '1': makeOntimeBlock({ id: '1', entries: [] }), '2': makeOntimeEvent({ id: '2', parent: null }), '3': makeOntimeEvent({ id: '3', parent: null }), }, @@ -863,7 +863,7 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1', '2']); expect(rundown.entries['1']).toMatchObject({ - events: ['3'], + entries: ['3'], }); expect(rundown.entries['3']).toMatchObject({ parent: '1', @@ -875,7 +875,7 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '11', '2'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['11'] }), + '1': makeOntimeBlock({ id: '1', entries: ['11'] }), '11': makeOntimeEvent({ id: '11', parent: '1' }), '2': makeOntimeEvent({ id: '2', parent: null }), }, @@ -885,7 +885,7 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1']); expect(rundown.entries['1']).toMatchObject({ - events: ['2', '11'], + entries: ['2', '11'], }); expect(rundown.entries['2']).toMatchObject({ parent: '1', @@ -931,7 +931,7 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '11', '2'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['11'] }), + '1': makeOntimeBlock({ id: '1', entries: ['11'] }), '11': makeOntimeEvent({ id: '11', parent: '1' }), '2': makeOntimeEvent({ id: '2', parent: null }), }, @@ -941,7 +941,7 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1', '11', '2']); expect(rundown.entries['1']).toMatchObject({ - events: [], + entries: [], }); expect(rundown.entries['2']).toMatchObject({ parent: null, @@ -953,9 +953,9 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '11', '2', '22'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['11'] }), + '1': makeOntimeBlock({ id: '1', entries: ['11'] }), '11': makeOntimeEvent({ id: '11', parent: '1' }), - '2': makeOntimeBlock({ id: '2', events: ['22'] }), + '2': makeOntimeBlock({ id: '2', entries: ['22'] }), '22': makeOntimeEvent({ id: '22', parent: '2' }), }, }); @@ -964,10 +964,10 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1', '2']); expect(rundown.entries['1']).toMatchObject({ - events: [], + entries: [], }); expect(rundown.entries['2']).toMatchObject({ - events: ['11', '22'], + entries: ['11', '22'], }); expect(rundown.entries['11']).toMatchObject({ parent: '2', @@ -979,8 +979,8 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '2', '22'], entries: { - '1': makeOntimeBlock({ id: '1', events: [] }), - '2': makeOntimeBlock({ id: '2', events: ['22'] }), + '1': makeOntimeBlock({ id: '1', entries: [] }), + '2': makeOntimeBlock({ id: '2', entries: ['22'] }), '22': makeOntimeEvent({ id: '22', parent: '2' }), }, }); @@ -989,10 +989,10 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1', '2']); expect(rundown.entries['1']).toMatchObject({ - events: ['22'], + entries: ['22'], }); expect(rundown.entries['2']).toMatchObject({ - events: [], + entries: [], }); expect(rundown.entries['22']).toMatchObject({ parent: '1', @@ -1004,9 +1004,9 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '11', '2', '22'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['11'] }), + '1': makeOntimeBlock({ id: '1', entries: ['11'] }), '11': makeOntimeEvent({ id: '11', parent: '1' }), - '2': makeOntimeBlock({ id: '2', events: ['22'] }), + '2': makeOntimeBlock({ id: '2', entries: ['22'] }), '22': makeOntimeEvent({ id: '22', parent: '2' }), }, }); @@ -1017,13 +1017,13 @@ describe('rundownMutation.reorder()', () => { // expect(newRundown.flatOrder).toStrictEqual(['1', '2', '11', '22']); // expect(changeList).toStrictEqual(['1', '2', '11', '22']); expect(rundown.entries['1']).toMatchObject({ - events: ['11'], + entries: ['11'], }); expect(rundown.entries['11']).toMatchObject({ parent: '1', }); expect(rundown.entries['2']).toMatchObject({ - events: [], + entries: [], }); expect(rundown.entries['22']).toMatchObject({ parent: null, @@ -1035,9 +1035,9 @@ describe('rundownMutation.reorder()', () => { order: ['1', '2'], flatOrder: ['1', '11', '2', '22'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['11'] }), + '1': makeOntimeBlock({ id: '1', entries: ['11'] }), '11': makeOntimeEvent({ id: '11', parent: '1' }), - '2': makeOntimeBlock({ id: '2', events: ['22'] }), + '2': makeOntimeBlock({ id: '2', entries: ['22'] }), '22': makeOntimeEvent({ id: '22', parent: '2' }), }, }); @@ -1046,13 +1046,13 @@ describe('rundownMutation.reorder()', () => { expect(rundown.order).toStrictEqual(['1', '11', '2']); expect(rundown.entries['1']).toMatchObject({ - events: [], + entries: [], }); expect(rundown.entries['11']).toMatchObject({ parent: null, }); expect(rundown.entries['2']).toMatchObject({ - events: ['22'], + entries: ['22'], }); expect(rundown.entries['22']).toMatchObject({ parent: '2', @@ -1374,7 +1374,7 @@ describe('rundownMutation.applyDelay()', () => { order: ['1', 'block', '2', '3'], entries: { '1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }), - block: makeOntimeBlock({ id: 'block', events: ['delay'] }), + block: makeOntimeBlock({ id: 'block', entries: ['delay'] }), delay: makeOntimeDelay({ id: 'delay', duration: 100, parent: 'block' }), '2': makeOntimeEvent({ id: '2', timeStart: 100, timeEnd: 200, duration: 100, linkStart: true }), '3': makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 300, duration: 100, linkStart: true }), @@ -1417,7 +1417,7 @@ describe('rundownMutation.applyDelay()', () => { entries: { '1': makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }), delay: makeOntimeDelay({ id: 'delay', duration: 100 }), - block: makeOntimeBlock({ id: 'block', events: ['block-1'] }), + block: makeOntimeBlock({ id: 'block', entries: ['block-1'] }), 'block-1': makeOntimeEvent({ id: 'block-1', timeStart: 100, @@ -1520,7 +1520,7 @@ describe('rundownMutation.clone()', () => { const testRundown = makeRundown({ order: ['1'], entries: { - '1': makeOntimeBlock({ id: '1', events: ['1a'] }), + '1': makeOntimeBlock({ id: '1', entries: ['1a'] }), '1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }), }, }); @@ -1528,7 +1528,7 @@ describe('rundownMutation.clone()', () => { const newEntry = rundownMutation.clone(testRundown, testRundown.entries['1a']); expect(testRundown.order).toStrictEqual(['1']); - expect(testRundown.entries['1']).toMatchObject({ events: ['1a', newEntry.id] }); + expect(testRundown.entries['1']).toMatchObject({ entries: ['1a', newEntry.id] }); expect(testRundown.entries[newEntry.id]).toMatchObject({ type: SupportedEntry.Event, parent: '1', @@ -1540,7 +1540,7 @@ describe('rundownMutation.clone()', () => { const testRundown = makeRundown({ order: ['1'], entries: { - '1': makeOntimeBlock({ id: '1', title: 'top', events: ['1a'] }), + '1': makeOntimeBlock({ id: '1', title: 'top', entries: ['1a'] }), '1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }), }, }); @@ -1550,9 +1550,9 @@ describe('rundownMutation.clone()', () => { expect(testRundown.order).toStrictEqual(['1', newEntry.id]); expect(testRundown.entries[newEntry.id]).toMatchObject({ type: SupportedEntry.Block, - events: [expect.any(String)], + entries: [expect.any(String)], }); - expect((testRundown.entries[newEntry.id] as OntimeBlock).events[0]).not.toBe('1a'); + expect((testRundown.entries[newEntry.id] as OntimeBlock).entries[0]).not.toBe('1a'); }); }); @@ -1575,7 +1575,7 @@ describe('rundownMutation.group()', () => { expect(rundown.entries).toMatchObject({ [blockId]: { type: SupportedEntry.Block, - events: ['1', '2'], + entries: ['1', '2'], }, '1': { id: '1', type: SupportedEntry.Event, parent: blockId }, '2': { id: '2', type: SupportedEntry.Event, parent: blockId }, @@ -1590,7 +1590,7 @@ describe('rundownMutation.ungroup()', () => { order: ['1', '2'], entries: { '1': makeOntimeEvent({ id: '1', cue: 'data1', parent: null }), - '2': makeOntimeBlock({ id: '2', events: ['21', '22'] }), + '2': makeOntimeBlock({ id: '2', entries: ['21', '22'] }), '21': makeOntimeEvent({ id: '21', cue: 'data21', parent: '2' }), '22': makeOntimeEvent({ id: '22', cue: 'data22', parent: '2' }), }, diff --git a/apps/server/src/api-data/rundown/__tests__/rundown.parser.test.ts b/apps/server/src/api-data/rundown/__tests__/rundown.parser.test.ts index 1930bbeea..f8c7c9aea 100644 --- a/apps/server/src/api-data/rundown/__tests__/rundown.parser.test.ts +++ b/apps/server/src/api-data/rundown/__tests__/rundown.parser.test.ts @@ -47,7 +47,7 @@ describe('parseRundown()', () => { flatOrder: ['1', '2', '3', '4'], entries: { '1': { id: '1', type: SupportedEntry.Event, title: 'test', skip: false } as OntimeEvent, // OK - '2': { id: '1', type: SupportedEntry.Block, title: 'test 2', skip: false } as OntimeBlock, // duplicate ID + '2': { id: '1', type: SupportedEntry.Block, title: 'test 2' } as OntimeBlock, // duplicate ID '3': {} as OntimeEvent, // no data '4': { id: '4', title: 'test 2', skip: false } as OntimeEvent, // no type }, @@ -220,7 +220,7 @@ describe('parseRundown()', () => { order: ['block'], flatOrder: ['block'], entries: { - block: makeOntimeBlock({ id: 'block', events: ['1', '2'] }), + block: makeOntimeBlock({ id: 'block', entries: ['1', '2'] }), '1': makeOntimeEvent({ id: '1' }), '2': makeOntimeEvent({ id: '2' }), }, @@ -229,7 +229,7 @@ describe('parseRundown()', () => { const parsedRundown = parseRundown(rundown, {}); expect(parsedRundown.order.length).toEqual(1); - expect(parsedRundown.entries.block).toMatchObject({ events: ['1', '2'] }); + expect(parsedRundown.entries.block).toMatchObject({ entries: ['1', '2'] }); expect(Object.keys(parsedRundown.entries).length).toEqual(3); }); }); diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index c598ec167..d82d37bf0 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -48,7 +48,7 @@ const cachedRundown: Rundown = { id: '', title: '', order: [], - flatOrder: [], // TODO: remove in favour of the metadata flatEntryOrder + flatOrder: [], entries: {}, revision: 0, }; @@ -150,7 +150,7 @@ export function createTransaction(options: TransactionOptions): Transaction { cachedRundown.title = rundown.title; cachedRundown.entries = entries; cachedRundown.order = order; - cachedRundown.flatOrder = metadata.flatEntryOrder; // TODO: remove in favour of the metadata flatEntryOrder + cachedRundown.flatOrder = metadata.flatEntryOrder; customFieldsMetadata.assigned = assignedCustomFields; rundownMetadata = metadata; } @@ -194,12 +194,12 @@ function add(rundown: Rundown, entry: OntimeEntry, afterId: EntryId | null, pare // 1. inserting an entry inside a block const parentBlock = rundown.entries[parentId] as OntimeBlock; if (afterId) { - const atEventsIndex = parentBlock.events.indexOf(afterId) + 1; + const atEventsIndex = parentBlock.entries.indexOf(afterId) + 1; const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1; - parentBlock.events = insertAtIndex(atEventsIndex, entry.id, parentBlock.events); + parentBlock.entries = insertAtIndex(atEventsIndex, entry.id, parentBlock.entries); rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder); } else { - parentBlock.events = insertAtIndex(0, entry.id, parentBlock.events); + parentBlock.entries = insertAtIndex(0, entry.id, parentBlock.entries); const atFlatIndex = rundown.flatOrder.indexOf(parentId) + 1; rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder); } @@ -247,8 +247,8 @@ function edit(rundown: Rundown, patch: PatchWithId): { entry: OntimeEntry; didIn function remove(rundown: Rundown, entry: OntimeEntry) { if (isOntimeBlock(entry)) { // for ontime blocks, we need to iterate through the children and delete them - for (let i = 0; i < entry.events.length; i++) { - const nestedEntryId = entry.events[i]; + for (let i = 0; i < entry.entries.length; i++) { + const nestedEntryId = entry.entries[i]; deleteEntry(nestedEntryId); } } else if (entry.parent) { @@ -256,8 +256,8 @@ function remove(rundown: Rundown, entry: OntimeEntry) { const parentBlock = rundown.entries[entry.parent] as OntimeBlock; if (parentBlock) { // we call a mutation to the parent event to remove the entry from the events - const filteredEvents = deleteById(parentBlock.events, entry.id); - edit(rundown, { id: parentBlock.id, events: filteredEvents }); + const filteredEvents = deleteById(parentBlock.entries, entry.id); + edit(rundown, { id: parentBlock.id, entries: filteredEvents }); } } deleteEntry(entry.id); @@ -300,8 +300,8 @@ function reorder(rundown: Rundown, eventFrom: OntimeEntry, eventTo: OntimeEntry, eventFrom.parent = toParent; } - const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).events; - const destinationArray = toParent === null ? rundown.order : (rundown.entries[toParent] as OntimeBlock).events; + const sourceArray = fromParent === null ? rundown.order : (rundown.entries[fromParent] as OntimeBlock).entries; + const destinationArray = toParent === null ? rundown.order : (rundown.entries[toParent] as OntimeBlock).entries; const fromIndex = sourceArray.indexOf(eventFrom.id); const toIndex = (() => { @@ -442,8 +442,8 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry { const newBlock = cloneBlock(entry, getUniqueId(rundown)); const nestedIds: EntryId[] = []; - for (let i = 0; i < entry.events.length; i++) { - const nestedEntryId = entry.events[i]; + for (let i = 0; i < entry.entries.length; i++) { + const nestedEntryId = entry.entries[i]; const nestedEntry = rundown.entries[nestedEntryId]; if (!nestedEntry) { continue; @@ -461,7 +461,7 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry { // indexes + 1 since we are inserting after the cloned block const atIndex = rundown.order.indexOf(entry.id) + 1; - newBlock.events = nestedIds; + newBlock.entries = nestedIds; newBlock.title = `${entry.title || 'Untitled'} (copy)`; rundown.entries[newBlock.id] = newBlock; @@ -504,7 +504,7 @@ function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock { rundown.order = rundown.order.filter((id) => id !== entryId); } - newBlock.events = nestedEvents; + newBlock.entries = nestedEvents; const insertIndex = Math.max(0, firstIndex); // we have filtered the items from the order // we will insert them now, with only the block at top level ... @@ -519,7 +519,7 @@ function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock { */ function ungroup(rundown: Rundown, block: OntimeBlock) { // get the events from the block and merge them into the order where the block was - const nestedEvents = block.events; + const nestedEvents = block.entries; const blockIndex = rundown.order.indexOf(block.id); rundown.order.splice(blockIndex, 1, ...nestedEvents); @@ -714,8 +714,8 @@ export function processRundown( const blockEvents: EntryId[] = []; // check if the block contains nested entries - for (let j = 0; j < processedEntry.events.length; j++) { - const nestedEntryId = processedEntry.events[j]; + for (let j = 0; j < processedEntry.entries.length; j++) { + const nestedEntryId = processedEntry.entries[j]; const nestedEntry = initialRundown.entries[nestedEntryId]; if (!nestedEntry) { @@ -750,7 +750,7 @@ export function processRundown( processedEntry.startTime = blockStartTime; processedEntry.endTime = blockEndTime; processedEntry.isFirstLinked = isFirstLinked; - processedEntry.events = blockEvents; + processedEntry.entries = blockEvents; } } diff --git a/apps/server/src/api-data/rundown/rundown.parser.ts b/apps/server/src/api-data/rundown/rundown.parser.ts index 5b949abc9..4eb9837e1 100644 --- a/apps/server/src/api-data/rundown/rundown.parser.ts +++ b/apps/server/src/api-data/rundown/rundown.parser.ts @@ -116,8 +116,8 @@ export function parseRundown( } else if (isOntimeDelay(event)) { newEvent = { ...delayDef, duration: event.duration, id }; } else if (isOntimeBlock(event)) { - for (let i = 0; i < event.events.length; i++) { - const nestedEventId = event.events[i]; + for (let i = 0; i < event.entries.length; i++) { + const nestedEventId = event.entries[i]; const nestedEvent = rundown.entries[nestedEventId]; if (isOntimeEvent(nestedEvent)) { @@ -149,8 +149,8 @@ export function parseRundown( ...blockDef, title: event.title, note: event.note, - events: event.events?.filter((eventId) => Object.hasOwn(rundown.entries, eventId)) ?? [], - skip: event.skip, + entries: event.entries?.filter((eventId) => Object.hasOwn(rundown.entries, eventId)) ?? [], + isNextDay: event.isNextDay, colour: event.colour, custom: { ...event.custom }, id, diff --git a/apps/server/src/api-data/rundown/rundown.router.ts b/apps/server/src/api-data/rundown/rundown.router.ts index a75a55463..624034f7b 100644 --- a/apps/server/src/api-data/rundown/rundown.router.ts +++ b/apps/server/src/api-data/rundown/rundown.router.ts @@ -99,19 +99,15 @@ router.patch('/swap', rundownSwapValidator, async (req: Request, res: Response) => { - try { - const newRundown = await applyDelay(req.params.id); - res.status(200).send(newRundown); - } catch (error) { - const message = getErrorMessage(error); - res.status(400).send({ message }); - } - }, -); +router.patch('/applydelay/:id', paramsWithId, async (req: Request, res: Response) => { + try { + const newRundown = await applyDelay(req.params.id); + res.status(200).send(newRundown); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).send({ message }); + } +}); router.post('/clone/:id', paramsWithId, async (req: Request, res: Response) => { try { @@ -133,19 +129,15 @@ router.post('/group', rundownArrayOfIds, async (req: Request, res: Response) => { - try { - const newRundown = await ungroupEntries(req.params.id); - res.status(200).send(newRundown); - } catch (error) { - const message = getErrorMessage(error); - res.status(400).send({ message }); - } - }, -); +router.post('/ungroup/:id', paramsWithId, async (req: Request, res: Response) => { + try { + const newRundown = await ungroupEntries(req.params.id); + res.status(200).send(newRundown); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).send({ message }); + } +}); router.delete('/', rundownArrayOfIds, async (req: Request, res: Response) => { try { diff --git a/apps/server/src/api-data/rundown/rundown.service.ts b/apps/server/src/api-data/rundown/rundown.service.ts index 7fd4e6cf7..bb6152bc8 100644 --- a/apps/server/src/api-data/rundown/rundown.service.ts +++ b/apps/server/src/api-data/rundown/rundown.service.ts @@ -347,7 +347,7 @@ export async function cloneEntry(entryId: EntryId): Promise { // notify timer and external services of change if (isOntimeBlock(newEntry)) { - notifyChanges(rundownMetadata, revision, { timer: newEntry.events, external: true }); + notifyChanges(rundownMetadata, revision, { timer: newEntry.entries, external: true }); } else if (isOntimeEvent(newEntry)) { notifyChanges(rundownMetadata, revision, { timer: [newEntry.id], external: true }); } else if (isOntimeDelay(newEntry)) { diff --git a/apps/server/src/api-data/rundown/rundown.utils.ts b/apps/server/src/api-data/rundown/rundown.utils.ts index df009a605..9fbbda806 100644 --- a/apps/server/src/api-data/rundown/rundown.utils.ts +++ b/apps/server/src/api-data/rundown/rundown.utils.ts @@ -156,8 +156,9 @@ export function createBlock(patch?: Partial): OntimeBlock { type: SupportedEntry.Block, title: patch.title ?? '', note: patch.note ?? '', - events: patch.events ?? [], - skip: patch.skip ?? false, + entries: patch.entries ?? [], + isNextDay: patch.isNextDay ?? false, + targetDuration: patch.targetDuration ?? null, colour: makeString(patch.colour, ''), custom: patch.custom ?? {}, revision: 0, @@ -288,7 +289,7 @@ export function cloneBlock(entry: OntimeBlock, newId: EntryId): OntimeBlock { newEntry.id = newId; // in blocks, we need to remove the events references - newEntry.events = []; + newEntry.entries = []; newEntry.revision = 0; return newEntry; } diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts index b87a3e567..ab9137eb5 100644 --- a/apps/server/src/models/demoProject.ts +++ b/apps/server/src/models/demoProject.ts @@ -41,11 +41,12 @@ export const demoDb: DatabaseModel = { entries: { block: { type: SupportedEntry.Block, - events: ['32d31', '21cd2', '0b371', '3cd28', 'e457f'], + entries: ['32d31', '21cd2', '0b371', '3cd28', 'e457f'], id: 'block', title: 'Test Block', note: '', - skip: false, + isNextDay: false, + targetDuration: null, colour: 'hotpink', revision: 0, startTime: null, @@ -210,8 +211,9 @@ export const demoDb: DatabaseModel = { title: 'Lunch break', note: '', colour: '', - events: [], - skip: false, + entries: [], + isNextDay: false, + targetDuration: null, custom: {}, revision: 0, startTime: null, @@ -372,8 +374,9 @@ export const demoDb: DatabaseModel = { title: 'Afternoon break', note: '', colour: '', - events: [], - skip: false, + entries: [], + isNextDay: false, + targetDuration: null, custom: {}, revision: 0, startTime: null, diff --git a/apps/server/src/models/eventsDefinition.ts b/apps/server/src/models/eventsDefinition.ts index 6d5ad200d..e39f8b315 100644 --- a/apps/server/src/models/eventsDefinition.ts +++ b/apps/server/src/models/eventsDefinition.ts @@ -44,8 +44,9 @@ export const block: Omit = { type: SupportedEntry.Block, title: '', note: '', - events: [], - skip: false, + entries: [], + isNextDay: false, + targetDuration: null, colour: '', custom: {}, // !==== RUNTIME METADATA ====! // diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 40fa29044..a374c37c3 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -375,9 +375,9 @@ describe('loadBlock', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), - 1: makeOntimeBlock({ id: '1', events: ['11'] }), + 1: makeOntimeBlock({ id: '1', entries: ['11'] }), 11: makeOntimeEvent({ id: '11', parent: '1' }), - 2: makeOntimeBlock({ id: '2', events: [] }), + 2: makeOntimeBlock({ id: '2', entries: [] }), 3: makeOntimeEvent({ id: '3', parent: null }), }, order: ['0', '1', '2', '3'], @@ -400,9 +400,9 @@ describe('loadBlock', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), - 1: makeOntimeBlock({ id: '1', events: ['11'] }), + 1: makeOntimeBlock({ id: '1', entries: ['11'] }), 11: makeOntimeEvent({ id: '11', parent: '1' }), - 2: makeOntimeBlock({ id: '2', events: ['22'] }), + 2: makeOntimeBlock({ id: '2', entries: ['22'] }), 22: makeOntimeEvent({ id: '22', parent: '2' }), }, order: ['0', '1', '2'], @@ -425,9 +425,9 @@ describe('loadBlock', () => { const rundown = makeRundown({ entries: { 0: makeOntimeEvent({ id: '0', parent: null }), - 1: makeOntimeBlock({ id: '1', events: ['11'] }), + 1: makeOntimeBlock({ id: '1', entries: ['11'] }), 11: makeOntimeEvent({ id: '11', parent: '1' }), - 2: makeOntimeBlock({ id: '2', events: ['22'] }), + 2: makeOntimeBlock({ id: '2', entries: ['22'] }), 22: makeOntimeEvent({ id: '22', parent: '2' }), }, order: ['0', '1', '2'], @@ -452,7 +452,7 @@ describe('loadBlock', () => { test('from block to same block will keep startedAt', () => { const rundown = makeRundown({ entries: { - 0: makeOntimeBlock({ id: '0', events: ['1', '2'] }), + 0: makeOntimeBlock({ id: '0', entries: ['1', '2'] }), 1: makeOntimeEvent({ id: '1', parent: '0' }), 2: makeOntimeEvent({ id: '2', parent: '0' }), }, diff --git a/apps/server/test-db/db.json b/apps/server/test-db/db.json index 5b5391056..9607fb90a 100644 --- a/apps/server/test-db/db.json +++ b/apps/server/test-db/db.json @@ -168,7 +168,7 @@ "title": "Lunch break", "note": "", "colour": "", - "events": [], + "entries": [], "skip": false, "custom": {}, "revision": 0, @@ -323,7 +323,7 @@ "title": "Afternoon break", "note": "", "colour": "", - "events": [], + "entries": [], "skip": false, "custom": {}, "revision": 0, diff --git a/e2e/tests/fixtures/e2e-test-db.json b/e2e/tests/fixtures/e2e-test-db.json index 71115c1f8..ac8feda6c 100644 --- a/e2e/tests/fixtures/e2e-test-db.json +++ b/e2e/tests/fixtures/e2e-test-db.json @@ -186,7 +186,7 @@ "title": "Lunch break", "note": "", "colour": "", - "events": [], + "entries": [], "skip": false, "custom": {}, "revision": 0, @@ -341,7 +341,7 @@ "title": "Afternoon break", "note": "", "colour": "", - "events": [], + "entries": [], "skip": false, "custom": {}, "revision": 0, diff --git a/packages/types/src/definitions/core/OntimeEvent.type.ts b/packages/types/src/definitions/core/OntimeEvent.type.ts index 11610e4f7..1dd4da846 100644 --- a/packages/types/src/definitions/core/OntimeEvent.type.ts +++ b/packages/types/src/definitions/core/OntimeEvent.type.ts @@ -23,8 +23,9 @@ export type OntimeBlock = OntimeBaseEvent & { type: SupportedEntry.Block; title: string; note: string; - events: EntryId[]; - skip: boolean; + entries: EntryId[]; + isNextDay: boolean; + targetDuration: MaybeNumber; colour: string; custom: EntryCustomFields; // !==== RUNTIME METADATA ====! // diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts index ebe3710f7..280ba0881 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.test.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts @@ -340,7 +340,7 @@ describe('getLastEvent', () => { const testRundown = { entries: { 1: { id: '1', type: SupportedEntry.Event } as OntimeEvent, - block: { id: 'block', type: SupportedEntry.Block, events: ['21', '22', '23'] } as OntimeBlock, + block: { id: 'block', type: SupportedEntry.Block, entries: ['21', '22', '23'] } as OntimeBlock, 21: { id: '21', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent, 22: { id: '22', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent, 23: { id: '23', type: SupportedEntry.Event, parent: 'block' } as OntimeEvent,