mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 06:29:07 +00:00
feat: duplicate groups
This commit is contained in:
@@ -87,6 +87,13 @@ export async function requestApplyDelay(delayId: EntryId): Promise<AxiosResponse
|
|||||||
return axios.patch(`${rundownPath}/applydelay/${delayId}`);
|
return axios.patch(`${rundownPath}/applydelay/${delayId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request for cloning an entry
|
||||||
|
*/
|
||||||
|
export async function postCloneEntry(entryId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/clone/${entryId}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request for dissolving of a block
|
* HTTP request for dissolving of a block
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
deleteEntries,
|
deleteEntries,
|
||||||
patchReorderEntry,
|
patchReorderEntry,
|
||||||
postAddEntry,
|
postAddEntry,
|
||||||
|
postCloneEntry,
|
||||||
putBatchEditEvents,
|
putBatchEditEvents,
|
||||||
putEditEntry,
|
putEditEntry,
|
||||||
ReorderEntry,
|
ReorderEntry,
|
||||||
@@ -164,6 +165,29 @@ export const useEntryActions = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls mutation to clone a selection
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _cloneMutation = useMutation({
|
||||||
|
mutationFn: postCloneEntry,
|
||||||
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a selection
|
||||||
|
*/
|
||||||
|
const clone = useCallback(
|
||||||
|
async (entryId: EntryId) => {
|
||||||
|
try {
|
||||||
|
await _cloneMutation.mutateAsync(entryId);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error cloning entry', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_cloneMutation],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to update existing entry
|
* Calls mutation to update existing entry
|
||||||
* @private
|
* @private
|
||||||
@@ -735,6 +759,7 @@ export const useEntryActions = () => {
|
|||||||
addEntry,
|
addEntry,
|
||||||
applyDelay,
|
applyDelay,
|
||||||
batchUpdateEvents,
|
batchUpdateEvents,
|
||||||
|
clone,
|
||||||
deleteEntry,
|
deleteEntry,
|
||||||
deleteAllEntries,
|
deleteAllEntries,
|
||||||
dissolveBlock,
|
dissolveBlock,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types';
|
import { EndAction, EntryCustomFields, OntimeEvent, SupportedEntry, TimerType, TimeStrategy } from 'ontime-types';
|
||||||
|
|
||||||
import { cloneEvent } from '../eventsManager';
|
import { cloneEvent } from '../clone';
|
||||||
|
|
||||||
describe('cloneEvent()', () => {
|
describe('cloneEvent()', () => {
|
||||||
it('creates a stem from a given event', () => {
|
it('creates a stem from a given event', () => {
|
||||||
@@ -37,7 +37,7 @@ import useFollowComponent from '../../common/hooks/useFollowComponent';
|
|||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/clone';
|
||||||
|
|
||||||
import BlockBlock from './block-block/BlockBlock';
|
import BlockBlock from './block-block/BlockBlock';
|
||||||
import BlockEnd from './block-block/BlockEnd';
|
import BlockEnd from './block-block/BlockEnd';
|
||||||
@@ -329,7 +329,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (sortableData.length < 1) {
|
if (sortableData.length < 1) {
|
||||||
return <RundownEmpty handleAddNew={(type: SupportedEntry) => insertAtId({ type }, cursor)} />;
|
return <RundownEmpty handleAddNew={(type: SupportedEntry) => addEntry({ type })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. gather presentation options
|
// 1. gather presentation options
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||||
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/clone';
|
||||||
|
|
||||||
import DelayBlock from './delay-block/DelayBlock';
|
import DelayBlock from './delay-block/DelayBlock';
|
||||||
import EventBlock from './event-block/EventBlock';
|
import EventBlock from './event-block/EventBlock';
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { IoChevronDown, IoChevronUp, IoEllipsisHorizontal, IoReorderTwo } from 'react-icons/io5';
|
import {
|
||||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
IoChevronDown,
|
||||||
|
IoChevronUp,
|
||||||
|
IoDuplicateOutline,
|
||||||
|
IoEllipsisHorizontal,
|
||||||
|
IoFolderOpenOutline,
|
||||||
|
IoReorderTwo,
|
||||||
|
IoTrash,
|
||||||
|
} from 'react-icons/io5';
|
||||||
|
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Portal } from '@chakra-ui/react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { EntryId, OntimeBlock } from 'ontime-types';
|
import { EntryId, OntimeBlock } from 'ontime-types';
|
||||||
@@ -23,7 +31,7 @@ interface BlockBlockProps {
|
|||||||
export default function BlockBlock(props: BlockBlockProps) {
|
export default function BlockBlock(props: BlockBlockProps) {
|
||||||
const { data, hasCursor, collapsed, onCollapse } = props;
|
const { data, hasCursor, collapsed, onCollapse } = props;
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
const { dissolveBlock } = useEntryActions();
|
const { clone, dissolveBlock, deleteEntry } = useEntryActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes: dragAttributes,
|
attributes: dragAttributes,
|
||||||
@@ -52,6 +60,8 @@ export default function BlockBlock(props: BlockBlockProps) {
|
|||||||
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'default',
|
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasChildren = data.events.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx([style.block, hasCursor && style.hasCursor, !collapsed && style.expanded])}
|
className={cx([style.block, hasCursor && style.hasCursor, !collapsed && style.expanded])}
|
||||||
@@ -83,12 +93,24 @@ export default function BlockBlock(props: BlockBlockProps) {
|
|||||||
variant='ontime-ghosted'
|
variant='ontime-ghosted'
|
||||||
size='sm'
|
size='sm'
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<Portal>
|
||||||
<MenuItem onClick={() => dissolveBlock(data.id)}>Dissolve Block</MenuItem>
|
<MenuList>
|
||||||
</MenuList>
|
<MenuItem icon={<IoDuplicateOutline />} onClick={() => clone(data.id)}>
|
||||||
|
Clone Block
|
||||||
|
</MenuItem>
|
||||||
|
{hasChildren && (
|
||||||
|
<MenuItem icon={<IoFolderOpenOutline />} onClick={() => dissolveBlock(data.id)}>
|
||||||
|
Dissolve Block
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem icon={<IoTrash />} onClick={() => deleteEntry([data.id])}>
|
||||||
|
Delete Block
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Portal>
|
||||||
</Menu>
|
</Menu>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Dissolve'
|
aria-label='Collapse'
|
||||||
onClick={() => onCollapse(!collapsed, data.id)}
|
onClick={() => onCollapse(!collapsed, data.id)}
|
||||||
color='#e2e2e2' // $gray-200
|
color='#e2e2e2' // $gray-200
|
||||||
variant='ontime-ghosted'
|
variant='ontime-ghosted'
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
|
|||||||
import { isOntimeEvent, SupportedEntry } from 'ontime-types';
|
import { isOntimeEvent, SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
||||||
import { cloneEvent } from '../../../../common/utils/eventsManager';
|
import { cloneEvent } from '../../../../common/utils/clone';
|
||||||
|
|
||||||
interface CuesheetTableMenuActionsProps {
|
interface CuesheetTableMenuActionsProps {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getAll, deleteWithId, deleteAll } from './report.controller.js';
|
import { getAll, deleteWithId, deleteAll } from './report.controller.js';
|
||||||
import { paramsMustHaveEventId } from '../rundown/rundown.validation.js';
|
import { paramsMustHaveEntryId } from '../rundown/rundown.validation.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
router.get('/', getAll);
|
router.get('/', getAll);
|
||||||
|
|
||||||
router.delete('/all', deleteAll);
|
router.delete('/all', deleteAll);
|
||||||
router.delete('/:eventId', paramsMustHaveEventId, deleteWithId);
|
router.delete('/:eventId', paramsMustHaveEntryId, deleteWithId);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
groupEntries,
|
groupEntries,
|
||||||
reorderEntry,
|
reorderEntry,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
|
cloneEntry,
|
||||||
} from '../../services/rundown-service/RundownService.js';
|
} from '../../services/rundown-service/RundownService.js';
|
||||||
import { getEntryWithId, getCurrentRundown } from '../../services/rundown-service/rundownUtils.js';
|
import { getEntryWithId, getCurrentRundown } from '../../services/rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ export async function rundownSwap(req: Request, res: Response<MessageResponse |
|
|||||||
|
|
||||||
export async function rundownApplyDelay(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
export async function rundownApplyDelay(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
await applyDelay(req.params.eventId);
|
await applyDelay(req.params.entryId);
|
||||||
res.status(200).send({ message: 'Delay applied' });
|
res.status(200).send({ message: 'Delay applied' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
@@ -128,9 +129,19 @@ export async function rundownApplyDelay(req: Request, res: Response<MessageRespo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function rundownCloneEntry(req: Request, res: Response<Rundown | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const newRundown = await cloneEntry(req.params.entryId);
|
||||||
|
res.status(200).send(newRundown);
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(400).send({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function rundownDissolveBlock(req: Request, res: Response<Rundown | ErrorResponse>) {
|
export async function rundownDissolveBlock(req: Request, res: Response<Rundown | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const newRundown = await dissolveBlock(req.params.eventId);
|
const newRundown = await dissolveBlock(req.params.entryId);
|
||||||
res.status(200).send(newRundown);
|
res.status(200).send(newRundown);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
rundownAddToBlock,
|
rundownAddToBlock,
|
||||||
rundownApplyDelay,
|
rundownApplyDelay,
|
||||||
rundownBatchPut,
|
rundownBatchPut,
|
||||||
|
rundownCloneEntry,
|
||||||
rundownDelete,
|
rundownDelete,
|
||||||
rundownDissolveBlock,
|
rundownDissolveBlock,
|
||||||
rundownGetAll,
|
rundownGetAll,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
rundownSwap,
|
rundownSwap,
|
||||||
} from './rundown.controller.js';
|
} from './rundown.controller.js';
|
||||||
import {
|
import {
|
||||||
paramsMustHaveEventId,
|
paramsMustHaveEntryId,
|
||||||
rundownArrayOfIds,
|
rundownArrayOfIds,
|
||||||
rundownBatchPutValidator,
|
rundownBatchPutValidator,
|
||||||
rundownPostValidator,
|
rundownPostValidator,
|
||||||
@@ -29,7 +30,7 @@ export const router = express.Router();
|
|||||||
|
|
||||||
router.get('/', rundownGetAll);
|
router.get('/', rundownGetAll);
|
||||||
router.get('/current', rundownGetCurrent);
|
router.get('/current', rundownGetCurrent);
|
||||||
router.get('/:eventId', paramsMustHaveEventId, rundownGetById); // not used in Ontime frontend
|
router.get('/:eventId', paramsMustHaveEntryId, rundownGetById); // not used in Ontime frontend
|
||||||
|
|
||||||
router.post('/', rundownPostValidator, rundownPost);
|
router.post('/', rundownPostValidator, rundownPost);
|
||||||
|
|
||||||
@@ -38,8 +39,9 @@ router.put('/batch', rundownBatchPutValidator, rundownBatchPut);
|
|||||||
|
|
||||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
||||||
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
router.patch('/applydelay/:entryId', paramsMustHaveEntryId, rundownApplyDelay);
|
||||||
router.post('/dissolve/:eventId', paramsMustHaveEventId, rundownDissolveBlock);
|
router.post('/clone/:entryId', paramsMustHaveEntryId, rundownCloneEntry);
|
||||||
|
router.post('/dissolve/:entryId', paramsMustHaveEntryId, rundownDissolveBlock);
|
||||||
router.post('/group', rundownArrayOfIds, rundownAddToBlock);
|
router.post('/group', rundownArrayOfIds, rundownAddToBlock);
|
||||||
|
|
||||||
router.delete('/', rundownArrayOfIds, deletesEventById);
|
router.delete('/', rundownArrayOfIds, deletesEventById);
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export const rundownSwapValidator = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const paramsMustHaveEventId = [
|
export const paramsMustHaveEntryId = [
|
||||||
param('eventId').exists(),
|
param('entryId').exists(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { updateRundownData } from '../../stores/runtimeState.js';
|
|||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
import * as cache from './rundownCache.js';
|
import * as cache from './rundownCache.js';
|
||||||
import { getInsertionPosition } from './rundownUtils.js';
|
import { getPreviousId } from './rundownUtils.js';
|
||||||
|
|
||||||
type CompleteEntry<T> =
|
type CompleteEntry<T> =
|
||||||
T extends Partial<OntimeEvent>
|
T extends Partial<OntimeEvent>
|
||||||
@@ -70,11 +70,12 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeEntry
|
|||||||
throw new Error(`Event with ID ${eventData.id} already exists`);
|
throw new Error(`Event with ID ${eventData.id} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. if the user provides a parent (inside a group), we make sure it exists
|
// 2. if the user provides a parent (inside a group), we make sure it exists and it is a group
|
||||||
let parent: EntryId | null = null;
|
let parent: EntryId | null = null;
|
||||||
if ('parent' in eventData && eventData.parent != null) {
|
if ('parent' in eventData && eventData.parent != null) {
|
||||||
if (!cache.hasId(eventData.parent)) {
|
const maybeParent = cache.getCurrentRundown().entries[eventData.parent];
|
||||||
throw new Error(`Parent event with ID ${eventData.parent} not found`);
|
if (!maybeParent || !isOntimeBlock(maybeParent)) {
|
||||||
|
throw new Error(`Invalid parent event with ID ${eventData.parent}`);
|
||||||
}
|
}
|
||||||
parent = eventData.parent;
|
parent = eventData.parent;
|
||||||
}
|
}
|
||||||
@@ -91,14 +92,14 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeEntry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { afterId, atIndex } = getInsertionPosition(parent, eventData?.after, eventData?.before);
|
const afterId = getPreviousId(eventData?.after, eventData?.before);
|
||||||
|
|
||||||
// generate a fully formed entry from the patch
|
// generate a fully formed entry from the patch
|
||||||
const sanitisedEntry = generateEvent(eventData, afterId);
|
const sanitisedEntry = generateEvent(eventData, afterId);
|
||||||
|
|
||||||
// modify rundown
|
// modify rundown
|
||||||
const scopedMutation = cache.mutateCache(cache.add);
|
const scopedMutation = cache.mutateCache(cache.add);
|
||||||
const { newEvent } = await scopedMutation({ atIndex, parent, entry: sanitisedEntry });
|
const { newEvent } = await scopedMutation({ afterId, parent, entry: sanitisedEntry });
|
||||||
|
|
||||||
// notify runtime that rundown has changed
|
// notify runtime that rundown has changed
|
||||||
updateRuntimeOnChange();
|
updateRuntimeOnChange();
|
||||||
@@ -113,9 +114,9 @@ export async function addEvent(eventData: EventPostPayload): Promise<OntimeEntry
|
|||||||
/**
|
/**
|
||||||
* deletes event by its ID
|
* deletes event by its ID
|
||||||
*/
|
*/
|
||||||
export async function deleteEvent(eventIds: string[]) {
|
export async function deleteEvent(eventIds: EntryId[]) {
|
||||||
const scopedMutation = cache.mutateCache(cache.remove);
|
const scopedMutation = cache.mutateCache(cache.remove);
|
||||||
const { didMutate } = await scopedMutation({ eventIds });
|
const { didMutate, changeList } = await scopedMutation({ eventIds });
|
||||||
|
|
||||||
if (!didMutate) {
|
if (!didMutate) {
|
||||||
return;
|
return;
|
||||||
@@ -125,7 +126,7 @@ export async function deleteEvent(eventIds: string[]) {
|
|||||||
updateRuntimeOnChange();
|
updateRuntimeOnChange();
|
||||||
|
|
||||||
// notify timer and external services of change
|
// notify timer and external services of change
|
||||||
notifyChanges({ timer: eventIds, external: true });
|
notifyChanges({ timer: changeList, external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,6 +220,27 @@ export async function applyDelay(delayId: EntryId) {
|
|||||||
notifyChanges({ timer: true, external: true });
|
notifyChanges({ timer: true, external: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones an entry, ensuring that all dependencies are preserved
|
||||||
|
*/
|
||||||
|
export async function cloneEntry(entryId: EntryId) {
|
||||||
|
const scopedMutation = cache.mutateCache(cache.clone);
|
||||||
|
const { newRundown, newEvent } = await scopedMutation({ entryId });
|
||||||
|
|
||||||
|
// notify runtime that rundown has changed
|
||||||
|
updateRuntimeOnChange();
|
||||||
|
|
||||||
|
if (isOntimeBlock(newEvent)) {
|
||||||
|
notifyChanges({ timer: newEvent.events, external: true });
|
||||||
|
} else if (isOntimeEvent(newEvent)) {
|
||||||
|
notifyChanges({ timer: [newEvent.id], external: true });
|
||||||
|
} else if (isOntimeDelay(newEvent)) {
|
||||||
|
notifyChanges({ external: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRundown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a block from the rundown and moves all its children to the top level
|
* Deletes a block from the rundown and moves all its children to the top level
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
customFieldChangelog,
|
customFieldChangelog,
|
||||||
dissolveBlock,
|
dissolveBlock,
|
||||||
groupEntries,
|
groupEntries,
|
||||||
|
clone,
|
||||||
} from '../rundownCache.js';
|
} from '../rundownCache.js';
|
||||||
import { makeOntimeBlock, makeOntimeDelay, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
import { makeOntimeBlock, makeOntimeDelay, makeOntimeEvent, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||||
import { ProcessedRundownMetadata } from '../rundownCache.utils.js';
|
import type { ProcessedRundownMetadata } from '../rundownCache.utils.js';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
vi.mock('../../../classes/data-provider/DataProvider.js', () => {
|
||||||
@@ -625,13 +626,60 @@ describe('generate() v4', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('add() mutation', () => {
|
describe('add() mutation', () => {
|
||||||
test('adds an event to the rundown', () => {
|
test('adds an event an empty rundown', () => {
|
||||||
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||||
const rundown = makeRundown({});
|
const rundown = makeRundown({});
|
||||||
const { newRundown } = add({ atIndex: 0, entry: mockEvent, parent: null, rundown });
|
const { newRundown } = add({ afterId: undefined, entry: mockEvent, parent: null, rundown });
|
||||||
expect(newRundown.order.length).toBe(1);
|
expect(newRundown.order.length).toBe(1);
|
||||||
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('adds an event at the top if no afterId is given', () => {
|
||||||
|
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||||
|
const rundown = makeRundown({
|
||||||
|
flatOrder: ['1'],
|
||||||
|
order: ['1'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', cue: '1' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { newRundown } = add({ afterId: undefined, entry: mockEvent, parent: null, rundown });
|
||||||
|
expect(newRundown.order).toStrictEqual(['mock', '1']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['mock', '1']);
|
||||||
|
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds an event at the top of the block if no after is given', () => {
|
||||||
|
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||||
|
const rundown = makeRundown({
|
||||||
|
flatOrder: ['1', '1a'],
|
||||||
|
order: ['1'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1' }),
|
||||||
|
'1a': makeOntimeEvent({ id: '1a', parent: '1' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { newRundown } = add({ afterId: undefined, entry: mockEvent, parent: '1', rundown });
|
||||||
|
expect(newRundown.order).toStrictEqual(['1']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', 'mock', '1a']);
|
||||||
|
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds an event at the a given location inside a block', () => {
|
||||||
|
const mockEvent = makeOntimeEvent({ id: 'mock', cue: 'mock' });
|
||||||
|
const rundown = makeRundown({
|
||||||
|
flatOrder: ['1', '1a'],
|
||||||
|
order: ['1'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1' }),
|
||||||
|
'1a': makeOntimeEvent({ id: '1a', parent: '1' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { newRundown } = add({ afterId: '1a', entry: mockEvent, parent: '1', rundown });
|
||||||
|
expect(newRundown.order).toStrictEqual(['1']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', '1a', 'mock']);
|
||||||
|
expect(newRundown.entries['mock']).toMatchObject(mockEvent);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remove() mutation', () => {
|
describe('remove() mutation', () => {
|
||||||
@@ -767,6 +815,65 @@ describe('reorder() mutation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clone() mutation', () => {
|
||||||
|
it('clones an event and adds it to the rundown', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1'],
|
||||||
|
flatOrder: ['1'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', cue: 'data1', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown, newEvent } = clone({ rundown, entryId: '1' });
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', newEvent!.id]);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', newEvent!.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clones an event inside a block and adds it to the rundown', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1'],
|
||||||
|
flatOrder: ['1', '1a'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', events: ['1a'] }),
|
||||||
|
'1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown, newEvent } = clone({ rundown, entryId: '1a' });
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', '1a', newEvent!.id]);
|
||||||
|
expect(newRundown.entries['1']).toMatchObject({ events: ['1a', newEvent!.id] });
|
||||||
|
expect(newRundown.entries[newEvent!.id]).toMatchObject({
|
||||||
|
type: SupportedEntry.Event,
|
||||||
|
parent: '1',
|
||||||
|
cue: 'nested',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clones a block and its nested elements', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1'],
|
||||||
|
flatOrder: ['1', '1a'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeBlock({ id: '1', title: 'top', events: ['1a'] }),
|
||||||
|
'1a': makeOntimeEvent({ id: '1a', cue: 'nested', parent: '1' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown, newEvent } = clone({ rundown, entryId: '1' });
|
||||||
|
|
||||||
|
expect(newRundown.order).toStrictEqual(['1', newEvent!.id]);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual(['1', '1a', expect.any(String), expect.any(String)]);
|
||||||
|
expect(newRundown.entries[newEvent!.id]).toMatchObject({
|
||||||
|
type: SupportedEntry.Block,
|
||||||
|
events: [expect.any(String)],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('dissolveBlock() mutation', () => {
|
describe('dissolveBlock() mutation', () => {
|
||||||
it('should correctly dissolve a block into its events', () => {
|
it('should correctly dissolve a block into its events', () => {
|
||||||
const rundown = makeRundown({
|
const rundown = makeRundown({
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { getPreviousId } from '../rundownUtils.js';
|
||||||
|
|
||||||
|
// Mock cache module
|
||||||
|
vi.mock('../rundownCache.js', () => ({
|
||||||
|
getEventOrder: () => ({
|
||||||
|
flatOrder: ['a', 'b', 'c', 'd'],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('getPreviousId', () => {
|
||||||
|
it('returns afterId if provided', () => {
|
||||||
|
expect(getPreviousId('b')).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the previous id before beforeId if provided', () => {
|
||||||
|
expect(getPreviousId(undefined, 'c')).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined if neither afterId nor beforeId is provided', () => {
|
||||||
|
expect(getPreviousId()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined if beforeId is not found', () => {
|
||||||
|
expect(getPreviousId(undefined, 'z')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,7 +27,14 @@ import { createBlock, createPatch } from '../../api-data/rundown/rundown.utils.j
|
|||||||
|
|
||||||
import type { RundownMetadata } from './rundown.types.js';
|
import type { RundownMetadata } from './rundown.types.js';
|
||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
import { hasChanges, isDataStale, makeRundownMetadata, type ProcessedRundownMetadata } from './rundownCache.utils.js';
|
import {
|
||||||
|
cloneBlock,
|
||||||
|
cloneEntry,
|
||||||
|
hasChanges,
|
||||||
|
isDataStale,
|
||||||
|
makeRundownMetadata,
|
||||||
|
type ProcessedRundownMetadata,
|
||||||
|
} from './rundownCache.utils.js';
|
||||||
|
|
||||||
let currentRundownId: EntryId = '';
|
let currentRundownId: EntryId = '';
|
||||||
let currentRundown: Rundown = {
|
let currentRundown: Rundown = {
|
||||||
@@ -271,6 +278,7 @@ export function getMetadata(): Readonly<RundownMetadata & { revision: number }>
|
|||||||
|
|
||||||
export type RundownOrder = {
|
export type RundownOrder = {
|
||||||
order: EntryId[];
|
order: EntryId[];
|
||||||
|
flatOrder: EntryId[];
|
||||||
timedEventsOrder: EntryId[];
|
timedEventsOrder: EntryId[];
|
||||||
playableEventsOrder: EntryId[];
|
playableEventsOrder: EntryId[];
|
||||||
};
|
};
|
||||||
@@ -284,6 +292,7 @@ export function getEventOrder(): Readonly<RundownOrder> {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
order: currentRundown.order,
|
order: currentRundown.order,
|
||||||
|
flatOrder: currentRundown.flatOrder,
|
||||||
timedEventsOrder: rundownMetadata.timedEventOrder,
|
timedEventsOrder: rundownMetadata.timedEventOrder,
|
||||||
playableEventsOrder: rundownMetadata.playableEventOrder,
|
playableEventsOrder: rundownMetadata.playableEventOrder,
|
||||||
};
|
};
|
||||||
@@ -333,41 +342,69 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
|||||||
return scopedMutation;
|
return scopedMutation;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddArgs = MutationParams<{ atIndex: number; parent: EntryId | null; entry: OntimeEntry }>;
|
type AddArgs = MutationParams<{ afterId?: string; parent: EntryId | null; entry: OntimeEntry }>;
|
||||||
/**
|
/**
|
||||||
* Add entry to rundown
|
* Add entry to rundown, handles the following cases:
|
||||||
|
* - 1. add entry in block, after a given entry
|
||||||
|
* - 2. add entry in block, at the beginning
|
||||||
|
* - 3. add entry to the rundown, after a given entry
|
||||||
|
* - 4. add entry to the rundown, at the beginning
|
||||||
*/
|
*/
|
||||||
export function add({ rundown, atIndex, parent, entry }: AddArgs): Required<MutatingReturn> {
|
export function add({ rundown, afterId, parent, entry }: AddArgs): Required<MutatingReturn> {
|
||||||
const newEntry: OntimeEntry = { ...entry };
|
|
||||||
|
|
||||||
rundown.entries[newEntry.id] = newEntry;
|
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const parentBlock = rundown.entries[parent] as OntimeBlock;
|
const parentBlock = rundown.entries[parent] as OntimeBlock;
|
||||||
parentBlock.events = insertAtIndex(atIndex, newEntry.id, parentBlock.events);
|
if (afterId) {
|
||||||
|
const atEventsIndex = parentBlock.events.indexOf(afterId) + 1;
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
||||||
|
parentBlock.events = insertAtIndex(atEventsIndex, entry.id, parentBlock.events);
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
} else {
|
||||||
|
parentBlock.events = insertAtIndex(0, entry.id, parentBlock.events);
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(parent) + 1;
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rundown.order = insertAtIndex(atIndex, newEntry.id, rundown.order);
|
if (afterId) {
|
||||||
|
const atOrderIndex = rundown.order.indexOf(afterId) + 1;
|
||||||
|
const atFlatIndex = rundown.flatOrder.indexOf(afterId) + 1;
|
||||||
|
rundown.order = insertAtIndex(atOrderIndex, entry.id, rundown.order);
|
||||||
|
rundown.flatOrder = insertAtIndex(atFlatIndex, entry.id, rundown.flatOrder);
|
||||||
|
} else {
|
||||||
|
rundown.order = insertAtIndex(0, entry.id, rundown.order);
|
||||||
|
rundown.flatOrder = insertAtIndex(0, entry.id, rundown.flatOrder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// either way, we insert the entry into the rundown
|
||||||
|
rundown.entries[entry.id] = entry;
|
||||||
setIsStale();
|
setIsStale();
|
||||||
return { newRundown: rundown, changeList: [], newEvent: newEntry, didMutate: true };
|
return { newRundown: rundown, changeList: [], newEvent: entry, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveArgs = MutationParams<{ eventIds: EntryId[] }>;
|
type RemoveArgs = MutationParams<{ eventIds: EntryId[] }>;
|
||||||
/**
|
/**
|
||||||
* Remove entries in a rundown
|
* Remove entries in a rundown
|
||||||
* It needs to ensure that the parent block is updated
|
* It handles element relationships specifically when dealing with nested items
|
||||||
|
* - when removing a nested item, remove the reference from the parent block
|
||||||
|
* - when removing a block, remove all nested items
|
||||||
*/
|
*/
|
||||||
export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
||||||
let didMutate = false;
|
/**
|
||||||
|
* changelist will hold a list of entries that need to be removed
|
||||||
|
* it will then be returned to the caller as a list of actually deleted entries
|
||||||
|
*/
|
||||||
|
const changeList: EntryId[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < eventIds.length; i++) {
|
for (let i = 0; i < eventIds.length; i++) {
|
||||||
const entry = rundown.entries[eventIds[i]];
|
const entry = rundown.entries[eventIds[i]];
|
||||||
if (isOntimeBlock(entry) || !entry.parent) {
|
// add the top level entry to the changeList
|
||||||
// top level events can simply be removed from the order
|
changeList.push(entry.id);
|
||||||
// the deletion process and the flatOrder are handled globally
|
|
||||||
rundown.order = rundown.order.filter((id) => id !== eventIds[i]);
|
if (isOntimeBlock(entry)) {
|
||||||
} else {
|
// for ontime blocks, we need to iterate through the children and delete them
|
||||||
|
changeList.concat([...entry.events]);
|
||||||
|
} else if (entry.parent) {
|
||||||
|
// at this point, we are handling entries inside a block, so we need to remove the references
|
||||||
const parentBlock = rundown.entries[entry.parent] as OntimeBlock;
|
const parentBlock = rundown.entries[entry.parent] as OntimeBlock;
|
||||||
const parentEvents = parentBlock.events.filter((id) => id !== eventIds[i]);
|
const parentEvents = parentBlock.events.filter((id) => id !== eventIds[i]);
|
||||||
|
|
||||||
@@ -383,14 +420,19 @@ export function remove({ rundown, eventIds }: RemoveArgs): MutatingReturn {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
didMutate = true;
|
|
||||||
rundown.flatOrder = rundown.flatOrder.filter((id) => id !== eventIds[i]);
|
|
||||||
delete rundown.entries[eventIds[i]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete all entries in the changeList
|
||||||
|
for (let i = 0; i < changeList.length; i++) {
|
||||||
|
const entryId = changeList[i];
|
||||||
|
rundown.order = rundown.order.filter((id) => id !== entryId);
|
||||||
|
rundown.flatOrder = rundown.flatOrder.filter((id) => id !== entryId);
|
||||||
|
delete rundown.entries[entryId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const didMutate = changeList.length > 0;
|
||||||
if (didMutate) setIsStale();
|
if (didMutate) setIsStale();
|
||||||
return { newRundown: rundown, didMutate };
|
return { newRundown: rundown, didMutate, changeList };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,6 +559,55 @@ export function applyDelay({ rundown, delayId }: ApplyDelayArgs): MutatingReturn
|
|||||||
return { newRundown: rundown, didMutate: true };
|
return { newRundown: rundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CloneEntryArgs = MutationParams<{ entryId: EntryId }>;
|
||||||
|
/**
|
||||||
|
* Apply a delay
|
||||||
|
* Mutates the given rundown
|
||||||
|
*/
|
||||||
|
export function clone({ rundown, entryId }: CloneEntryArgs): MutatingReturn {
|
||||||
|
const entry = rundown.entries[entryId];
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error('Entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
|
const newBlock = cloneBlock(entry, getUniqueId());
|
||||||
|
const nestedIds: EntryId[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < entry.events.length; i++) {
|
||||||
|
const nestedEntryId = entry.events[i];
|
||||||
|
const nestedEntry = rundown.entries[nestedEntryId];
|
||||||
|
if (!nestedEntry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone the event and assign it to the new block
|
||||||
|
const newNestedEntry = cloneEntry(nestedEntry, getUniqueId());
|
||||||
|
(newNestedEntry as OntimeEvent | OntimeDelay).parent = newBlock.id;
|
||||||
|
|
||||||
|
nestedIds.push(newNestedEntry.id);
|
||||||
|
// we immediately insert the nested entries into the rundown
|
||||||
|
rundown.entries[newNestedEntry.id] = newNestedEntry;
|
||||||
|
}
|
||||||
|
// indexes + 1 since we are inserting after the cloned block
|
||||||
|
const atIndex = rundown.order.indexOf(entryId) + 1;
|
||||||
|
// we need to find the index of the last entry
|
||||||
|
const lastNestedIdInOriginal = entry.events.at(-1) ?? '0';
|
||||||
|
const flatIndex = rundown.flatOrder.indexOf(lastNestedIdInOriginal) + 1;
|
||||||
|
|
||||||
|
newBlock.events = nestedIds;
|
||||||
|
newBlock.title = `${entry.title} (copy)`;
|
||||||
|
|
||||||
|
rundown.entries[newBlock.id] = newBlock;
|
||||||
|
rundown.order = insertAtIndex(atIndex, newBlock.id, rundown.order);
|
||||||
|
rundown.flatOrder = mergeAtIndex(flatIndex, [newBlock.id, ...nestedIds], rundown.flatOrder);
|
||||||
|
|
||||||
|
return { newRundown: rundown, didMutate: true, newEvent: newBlock };
|
||||||
|
} else {
|
||||||
|
return add({ rundown, afterId: entryId, parent: entry.parent, entry: cloneEntry(entry, getUniqueId()) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type DissolveBlockArgs = MutationParams<{ blockId: EntryId }>;
|
type DissolveBlockArgs = MutationParams<{ blockId: EntryId }>;
|
||||||
/**
|
/**
|
||||||
* Deletes a block and moves all its children to the top level order
|
* Deletes a block and moves all its children to the top level order
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
PlayableEvent,
|
PlayableEvent,
|
||||||
RundownEntries,
|
RundownEntries,
|
||||||
|
OntimeDelay,
|
||||||
|
OntimeBlock,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { dayInMs, getLinkedTimes, getTimeFrom, isNewLatest } from 'ontime-utils';
|
import { dayInMs, getLinkedTimes, getTimeFrom, isNewLatest } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -286,3 +288,37 @@ function processEntry<T extends OntimeEntry>(
|
|||||||
|
|
||||||
return { processedData, processedEntry: currentEntry };
|
return { processedData, processedEntry: currentEntry };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cloneEvent(entry: OntimeEvent, newId: EntryId): OntimeEvent {
|
||||||
|
const newEntry = structuredClone(entry);
|
||||||
|
newEntry.id = newId;
|
||||||
|
newEntry.revision = 0;
|
||||||
|
return newEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneDelay(entry: OntimeDelay, newId: EntryId): OntimeDelay {
|
||||||
|
const newEntry = structuredClone(entry);
|
||||||
|
newEntry.id = newId;
|
||||||
|
return newEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneBlock(entry: OntimeBlock, newId: EntryId): OntimeBlock {
|
||||||
|
const newEntry = structuredClone(entry);
|
||||||
|
newEntry.id = newId;
|
||||||
|
|
||||||
|
// in blocks, we need to remove the events references
|
||||||
|
newEntry.events = [];
|
||||||
|
newEntry.revision = 0;
|
||||||
|
return newEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneEntry<T extends OntimeEntry>(entry: T, newId: EntryId): T {
|
||||||
|
if (isOntimeEvent(entry)) {
|
||||||
|
return cloneEvent(entry, newId) as T;
|
||||||
|
} else if (isOntimeDelay(entry)) {
|
||||||
|
return cloneDelay(entry, newId) as T;
|
||||||
|
} else if (entry.type === 'block') {
|
||||||
|
return cloneBlock(entry as OntimeBlock, newId) as T;
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported entry type for cloning: ${entry}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
EntryId,
|
EntryId,
|
||||||
RundownEntries,
|
RundownEntries,
|
||||||
ProjectRundowns,
|
ProjectRundowns,
|
||||||
OntimeBlock,
|
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import * as cache from './rundownCache.js';
|
import * as cache from './rundownCache.js';
|
||||||
@@ -175,37 +174,21 @@ export function getRundownOrThrow(rundowns: ProjectRundowns, rundownId: string):
|
|||||||
return rundowns[rundownId];
|
return rundowns[rundownId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInsertionPosition(
|
/**
|
||||||
parentId: EntryId | null,
|
* Receives an insertion order and returns the reference to an event ID
|
||||||
afterId?: EntryId,
|
* after which we will insert the new event
|
||||||
beforeId?: EntryId,
|
*/
|
||||||
): { atIndex: number; afterId: EntryId | undefined } {
|
export function getPreviousId(afterId?: EntryId, beforeId?: EntryId): EntryId | undefined {
|
||||||
if (afterId) {
|
if (afterId) {
|
||||||
const order = selectOrderList(parentId);
|
return afterId;
|
||||||
return {
|
|
||||||
atIndex: order.findIndex((id) => id === afterId) + 1,
|
|
||||||
afterId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
const order = selectOrderList(parentId);
|
const flatOrder = cache.getEventOrder().flatOrder;
|
||||||
const atIndex = order.findIndex((id) => id === beforeId);
|
const atIndex = flatOrder.findIndex((id) => id === beforeId);
|
||||||
return {
|
if (atIndex < 1) return undefined;
|
||||||
atIndex,
|
return flatOrder[atIndex - 1];
|
||||||
afterId: order[atIndex - 1] ?? null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return;
|
||||||
atIndex: 0,
|
|
||||||
afterId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
function selectOrderList(parentId: EntryId | null) {
|
|
||||||
if (parentId) {
|
|
||||||
return (getEntryWithId(parentId) as OntimeBlock).events;
|
|
||||||
}
|
|
||||||
return cache.getEventOrder().order;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
EndAction,
|
EndAction,
|
||||||
|
EntryId,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isPlayableEvent,
|
isPlayableEvent,
|
||||||
LogOrigin,
|
LogOrigin,
|
||||||
@@ -238,7 +239,7 @@ class RuntimeService {
|
|||||||
* Called when the underlying data has changed,
|
* Called when the underlying data has changed,
|
||||||
* we check if the change affects the runtime
|
* we check if the change affects the runtime
|
||||||
*/
|
*/
|
||||||
public notifyOfChangedEvents(affectedIds?: string[]) {
|
public notifyOfChangedEvents(affectedIds?: EntryId[]) {
|
||||||
const state = runtimeState.getState();
|
const state = runtimeState.getState();
|
||||||
const hasLoadedElements = state.eventNow !== null || state.eventNext !== null;
|
const hasLoadedElements = state.eventNow !== null || state.eventNext !== null;
|
||||||
if (!hasLoadedElements) {
|
if (!hasLoadedElements) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { deleteAtIndex, insertAtIndex, reorderArray } from './arrayUtils.js';
|
import { deleteAtIndex, insertAtIndex, mergeAtIndex, reorderArray } from './arrayUtils.js';
|
||||||
|
|
||||||
describe('insertAtIndex', () => {
|
describe('insertAtIndex', () => {
|
||||||
it('should insert an item at the beginning of the array', () => {
|
it('should insert an item at the beginning of the array', () => {
|
||||||
@@ -23,7 +23,34 @@ describe('insertAtIndex', () => {
|
|||||||
const array = [1, 2, 3];
|
const array = [1, 2, 3];
|
||||||
const result = insertAtIndex(1, 5, array);
|
const result = insertAtIndex(1, 5, array);
|
||||||
expect(result).toEqual([1, 5, 2, 3]);
|
expect(result).toEqual([1, 5, 2, 3]);
|
||||||
expect(array).toEqual([1, 2, 3]); // Original array should remain unchanged
|
expect(array).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeAtIndex', () => {
|
||||||
|
it('should insert an item at the beginning of the array', () => {
|
||||||
|
const array = ['1', '2', '3'];
|
||||||
|
const result = mergeAtIndex(0, ['a', 'b'], array);
|
||||||
|
expect(result).toEqual(['a', 'b', '1', '2', '3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert an item at the end of the array', () => {
|
||||||
|
const array = ['1', '2', '3'];
|
||||||
|
const result = mergeAtIndex(3, ['a', 'b'], array);
|
||||||
|
expect(result).toEqual(['1', '2', '3', 'a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert an item in the middle of the array', () => {
|
||||||
|
const array = ['1', '2', '3'];
|
||||||
|
const result = mergeAtIndex(2, ['a', 'b'], array);
|
||||||
|
expect(result).toEqual(['1', '2', 'a', 'b', '3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a new array and not modify the original array', () => {
|
||||||
|
const array = ['1', '2', '3'];
|
||||||
|
const result = mergeAtIndex(5, ['a', 'b'], array);
|
||||||
|
expect(result).toEqual(['1', '2', '3', 'a', 'b']);
|
||||||
|
expect(array).toEqual(['1', '2', '3']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user