refactor: implement operations on nested events

This commit is contained in:
Carlos Valente
2025-04-17 09:36:22 +02:00
committed by arc-alex
parent 6fb42b989e
commit dd6d74121c
10 changed files with 257 additions and 139 deletions
+28 -23
View File
@@ -8,6 +8,7 @@ import {
type Rundown,
isOntimeBlock,
isOntimeEvent,
OntimeEntry,
Playback,
SupportedEvent,
} from 'ontime-types';
@@ -93,7 +94,7 @@ export default function Rundown({ data }: RundownProps) {
);
const insertAtId = useCallback(
(type: SupportedEvent, id: MaybeString, above = false) => {
(patch: Partial<OntimeEntry> & { type: SupportedEvent }, id: MaybeString, above = false) => {
const options: EventOptions =
id === null
? {}
@@ -102,17 +103,10 @@ export default function Rundown({ data }: RundownProps) {
before: above ? id : undefined,
};
if (type === SupportedEvent.Event) {
const newEvent = {
type: SupportedEvent.Event,
};
if (!above && id) {
options.lastEventId = id;
}
addEntry(newEvent, options);
} else {
addEntry({ type }, options);
if (!above && id) {
options.lastEventId = id;
}
addEntry(patch, options);
},
[addEntry],
);
@@ -208,14 +202,14 @@ export default function Rundown({ data }: RundownProps) {
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }],
['alt + E', () => insertAtId(SupportedEvent.Event, cursor), { preventDefault: true }],
['alt + shift + E', () => insertAtId(SupportedEvent.Event, cursor, true), { preventDefault: true }],
['alt + E', () => insertAtId({ type: SupportedEvent.Event }, cursor), { preventDefault: true }],
['alt + shift + E', () => insertAtId({ type: SupportedEvent.Event }, cursor, true), { preventDefault: true }],
['alt + B', () => insertAtId(SupportedEvent.Block, cursor), { preventDefault: true }],
['alt + shift + B', () => insertAtId(SupportedEvent.Block, cursor, true), { preventDefault: true }],
['alt + B', () => insertAtId({ type: SupportedEvent.Block }, cursor), { preventDefault: true }],
['alt + shift + B', () => insertAtId({ type: SupportedEvent.Block }, cursor, true), { preventDefault: true }],
['alt + D', () => insertAtId(SupportedEvent.Delay, cursor), { preventDefault: true }],
['alt + shift + D', () => insertAtId(SupportedEvent.Delay, cursor, true), { preventDefault: true }],
['alt + D', () => insertAtId({ type: SupportedEvent.Delay }, cursor), { preventDefault: true }],
['alt + shift + D', () => insertAtId({ type: SupportedEvent.Delay }, cursor, true), { preventDefault: true }],
['mod + C', () => setEntryCopyId(cursor)],
['mod + V', () => insertCopyAtId(cursor, entryCopyId)],
@@ -260,7 +254,7 @@ export default function Rundown({ data }: RundownProps) {
};
if (statefulEntries.length < 1) {
return <RundownEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />;
return <RundownEmpty handleAddNew={() => insertAtId({ type: SupportedEvent.Event }, cursor)} />;
}
// 1. gather presentation options
@@ -292,15 +286,21 @@ export default function Rundown({ data }: RundownProps) {
return (
<Fragment key={entry.id}>
{isEditMode && (hasCursor || isFirst) && (
<QuickAddBlock showBlocks previousEventId={rundownMeta.previousEntryId} />
<QuickAddBlock previousEventId={rundownMeta.previousEntryId} parentBlock={null} />
)}
{isOntimeBlock(entry) ? (
<BlockBlock data={entry} hasCursor={hasCursor}>
{entry.events.length === 0 && (
<BlockEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />
<BlockEmpty
handleAddNew={() => insertAtId({ type: SupportedEvent.Event, parent: entry.id }, entry.id)}
/>
)}
{entry.events.map((eventId, nestedIndex) => {
const nestedEntry = entries[eventId];
if (!nestedEntry) {
return null;
}
const nestedRundownMeta = process(nestedEntry);
const isFirstInGroup = nestedIndex === 0;
const isLastInGroup = nestedIndex === entry.events.length - 1;
@@ -312,7 +312,10 @@ export default function Rundown({ data }: RundownProps) {
return (
<Fragment key={nestedEntry.id}>
{isEditMode && (hasNestedCursor || isFirstInGroup) && (
<QuickAddBlock previousEventId={rundownMeta.previousEntryId} />
<QuickAddBlock
parentBlock={entry.id}
previousEventId={nestedRundownMeta.previousEntryId}
/>
)}
<div
@@ -342,7 +345,7 @@ export default function Rundown({ data }: RundownProps) {
</div>
</div>
{isEditMode && (hasNestedCursor || isLastInGroup) && (
<QuickAddBlock previousEventId={entry.id} />
<QuickAddBlock parentBlock={entry.id} previousEventId={nestedEntry.id} />
)}
</Fragment>
);
@@ -371,7 +374,9 @@ export default function Rundown({ data }: RundownProps) {
</div>
</div>
)}
{isEditMode && (hasCursor || isLast) && <QuickAddBlock showBlocks previousEventId={entry.id} />}
{isEditMode && (hasCursor || isLast) && (
<QuickAddBlock previousEventId={entry.id} parentBlock={null} />
)}
</Fragment>
);
})}
@@ -5,6 +5,7 @@
margin: 0.25rem 0;
font-size: calc(1rem - 3px);
margin-left: calc(2em + 0.5rem);
}
.quickBtn {
@@ -1,74 +1,69 @@
import { memo, useCallback, useRef } from 'react';
import { memo, useRef } from 'react';
import { IoAdd } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import { MaybeString, SupportedEvent } from 'ontime-types';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { useEmitLog } from '../../../common/stores/logger';
import style from './QuickAddBlock.module.scss';
interface QuickAddBlockProps {
previousEventId: MaybeString;
showBlocks?: boolean;
parentBlock: MaybeString;
}
export default memo(QuickAddBlock);
function QuickAddBlock(props: QuickAddBlockProps) {
const { previousEventId, showBlocks } = props;
const { previousEventId, parentBlock } = props;
const { addEntry } = useEntryActions();
const { emitError } = useEmitLog();
const doLinkPrevious = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null);
const handleCreateEvent = useCallback(
(eventType: SupportedEvent) => {
switch (eventType) {
case 'event': {
const defaultPublic = doPublic?.current?.checked;
const linkPrevious = doLinkPrevious?.current?.checked;
const addEvent = () => {
addEntry(
{
type: SupportedEvent.Event,
parent: parentBlock ?? null,
},
{
after: previousEventId,
defaultPublic: doPublic?.current?.checked,
lastEventId: previousEventId,
linkPrevious: doLinkPrevious?.current?.checked,
},
);
};
const newEvent = { type: SupportedEvent.Event };
const options = {
after: previousEventId,
defaultPublic,
lastEventId: previousEventId,
linkPrevious,
};
addEntry(newEvent, options);
break;
}
case 'delay': {
const options = {
lastEventId: previousEventId,
after: previousEventId,
};
addEntry({ type: SupportedEvent.Delay }, options);
break;
}
case 'block': {
const options = {
lastEventId: previousEventId,
after: previousEventId,
};
addEntry({ type: SupportedEvent.Block }, options);
break;
}
default: {
emitError(`Cannot create unknown event type: ${eventType}`);
break;
}
}
},
[previousEventId, addEntry, emitError],
);
const addDelay = () => {
addEntry(
// TODO(v4): add delays to blocks
{ type: SupportedEvent.Delay },
{
lastEventId: previousEventId,
after: previousEventId,
},
);
};
const addBlock = () => {
if (parentBlock !== null) {
return;
}
addEntry(
{ type: SupportedEvent.Block },
{
lastEventId: previousEventId,
after: previousEventId,
},
);
};
return (
<div className={style.quickAdd}>
<Button
onClick={() => handleCreateEvent(SupportedEvent.Event)}
onClick={addEvent}
size='xs'
variant='ontime-subtle-white'
className={style.quickBtn}
@@ -78,7 +73,7 @@ function QuickAddBlock(props: QuickAddBlockProps) {
Event
</Button>
<Button
onClick={() => handleCreateEvent(SupportedEvent.Delay)}
onClick={addDelay}
size='xs'
variant='ontime-subtle-white'
className={style.quickBtn}
@@ -87,9 +82,9 @@ function QuickAddBlock(props: QuickAddBlockProps) {
>
Delay
</Button>
{showBlocks && (
{parentBlock === null && (
<Button
onClick={() => handleCreateEvent(SupportedEvent.Block)}
onClick={addBlock}
size='xs'
variant='ontime-subtle-white'
className={style.quickBtn}
@@ -29,7 +29,7 @@ export function makeRundownMetadata(selectedEventId: MaybeString) {
isNext: false,
isNextDay: false,
totalGap: 0,
isLinkedToLoaded: true,
isLinkedToLoaded: false,
isLoaded: false,
};
@@ -56,6 +56,7 @@ function processEntry(
processedData.isLoaded = false;
processedData.previousEntryId = processedData.thisId;
processedData.thisId = entry.id;
processedData.previousEvent = processedData.latestEvent;
if (entry.id === selectedEventId) {
processedData.isLoaded = true;
@@ -65,19 +66,18 @@ function processEntry(
if (isOntimeEvent(entry)) {
// event indexes are 1 based in UI
processedData.eventIndex += 1;
processedData.previousEvent = processedData.latestEvent;
if (isPlayableEvent(entry)) {
processedData.isNextDay = checkIsNextDay(entry, processedData.previousEvent);
processedData.totalGap += entry.gap;
if (!processedData.isPast) {
processedData.totalGap += entry.gap;
if (!processedData.isPast && !processedData.isLoaded) {
/**
* isLinkToLoaded is a chain value that we maintain until we find an unlinked event
* or we find a countToEnd event
* isLinkToLoaded is a chain value that we maintain until we
* a) find an unlinked event
* b) find a countToEnd event
*/
processedData.isLinkedToLoaded =
processedData.isLinkedToLoaded && entry.linkStart && !processedData.previousEvent?.countToEnd;
processedData.isLinkedToLoaded = entry.linkStart && !processedData.previousEvent?.countToEnd;
}
if (isNewLatest(entry, processedData.previousEvent)) {