mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
feat: create group from entry selection
This commit is contained in:
committed by
Carlos Valente
parent
67f914bdc0
commit
7d5291e5e5
@@ -90,15 +90,15 @@ export async function requestApplyDelay(delayId: EntryId): Promise<AxiosResponse
|
|||||||
/**
|
/**
|
||||||
* HTTP request for dissolving of a block
|
* HTTP request for dissolving of a block
|
||||||
*/
|
*/
|
||||||
export async function requestDissolveBlock(blockId: EntryId): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestDissolveBlock(blockId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.post(`${rundownPath}/dissolve/${blockId}`);
|
return axios.post(`${rundownPath}/dissolve/${blockId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request for grouping a list of entries into a block
|
* HTTP request for grouping a list of entries into a block
|
||||||
*/
|
*/
|
||||||
export async function requestGroupEntries(entryIds: EntryId[]): Promise<AxiosResponse<MessageResponse>> {
|
export async function requestGroupEntries(entryIds: EntryId[]): Promise<AxiosResponse<Rundown>> {
|
||||||
return axios.post(`${rundownPath}/group`, { data: { ids: entryIds } });
|
return axios.post(`${rundownPath}/group`, { ids: entryIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
requestDeleteAll,
|
requestDeleteAll,
|
||||||
requestDissolveBlock,
|
requestDissolveBlock,
|
||||||
requestEventSwap,
|
requestEventSwap,
|
||||||
|
requestGroupEntries,
|
||||||
SwapEntry,
|
SwapEntry,
|
||||||
} from '../api/rundown';
|
} from '../api/rundown';
|
||||||
import { logAxiosError } from '../api/utils';
|
import { logAxiosError } from '../api/utils';
|
||||||
@@ -521,6 +522,19 @@ export const useEntryActions = () => {
|
|||||||
*/
|
*/
|
||||||
const _dissolveBlockMutation = useMutation({
|
const _dissolveBlockMutation = useMutation({
|
||||||
mutationFn: requestDissolveBlock,
|
mutationFn: requestDissolveBlock,
|
||||||
|
onSuccess: (response) => {
|
||||||
|
if (!response.data) return;
|
||||||
|
|
||||||
|
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
order,
|
||||||
|
flatOrder,
|
||||||
|
entries,
|
||||||
|
revision,
|
||||||
|
});
|
||||||
|
},
|
||||||
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -537,6 +551,43 @@ export const useEntryActions = () => {
|
|||||||
},
|
},
|
||||||
[_dissolveBlockMutation],
|
[_dissolveBlockMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls mutation to create a block with a selection
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _groupEntriesMutation = useMutation({
|
||||||
|
mutationFn: requestGroupEntries,
|
||||||
|
onSuccess: (response) => {
|
||||||
|
if (!response.data) return;
|
||||||
|
|
||||||
|
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||||
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
order,
|
||||||
|
flatOrder,
|
||||||
|
entries,
|
||||||
|
revision,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a block with a selection
|
||||||
|
*/
|
||||||
|
const groupEntries = useCallback(
|
||||||
|
async (entryIds: EntryId[]) => {
|
||||||
|
try {
|
||||||
|
await _groupEntriesMutation.mutateAsync(entryIds);
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosError('Error grouping entries', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[_groupEntriesMutation],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to reorder an entry
|
* Calls mutation to reorder an entry
|
||||||
* @private
|
* @private
|
||||||
@@ -575,17 +626,17 @@ export const useEntryActions = () => {
|
|||||||
|
|
||||||
// Mutation finished, we update the rundown with the response
|
// Mutation finished, we update the rundown with the response
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
if (response.data) {
|
if (!response.data) return;
|
||||||
const { id, title, order, flatOrder, entries, revision } = response.data;
|
|
||||||
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
const { id, title, order, flatOrder, entries, revision } = response.data;
|
||||||
id,
|
queryClient.setQueryData<Rundown>(RUNDOWN, {
|
||||||
title,
|
id,
|
||||||
order,
|
title,
|
||||||
flatOrder,
|
order,
|
||||||
entries,
|
flatOrder,
|
||||||
revision,
|
entries,
|
||||||
});
|
revision,
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
@@ -688,6 +739,7 @@ export const useEntryActions = () => {
|
|||||||
deleteAllEntries,
|
deleteAllEntries,
|
||||||
dissolveBlock,
|
dissolveBlock,
|
||||||
getEntryById,
|
getEntryById,
|
||||||
|
groupEntries,
|
||||||
reorderEntry,
|
reorderEntry,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
updateEntry,
|
updateEntry,
|
||||||
|
|||||||
@@ -19,18 +19,17 @@ import EventBlock from './event-block/EventBlock';
|
|||||||
import { useEventSelection } from './useEventSelection';
|
import { useEventSelection } from './useEventSelection';
|
||||||
|
|
||||||
export type EventItemActions =
|
export type EventItemActions =
|
||||||
| 'set-cursor'
|
|
||||||
| 'event'
|
| 'event'
|
||||||
| 'event-before'
|
| 'event-before'
|
||||||
| 'delay'
|
| 'delay'
|
||||||
| 'delay-before'
|
| 'delay-before'
|
||||||
| 'block'
|
| 'block'
|
||||||
| 'block-before'
|
| 'block-before'
|
||||||
|
| 'swap'
|
||||||
| 'delete'
|
| 'delete'
|
||||||
| 'clone'
|
| 'clone'
|
||||||
| 'update'
|
| 'group'
|
||||||
| 'swap'
|
| 'update';
|
||||||
| 'clear-report';
|
|
||||||
|
|
||||||
interface RundownEntryProps {
|
interface RundownEntryProps {
|
||||||
type: SupportedEntry;
|
type: SupportedEntry;
|
||||||
@@ -66,7 +65,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
isLinkedToLoaded,
|
isLinkedToLoaded,
|
||||||
} = props;
|
} = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { addEntry, updateEntry, batchUpdateEvents, deleteEntry, swapEvents } = useEntryActions();
|
const { addEntry, updateEntry, batchUpdateEvents, deleteEntry, groupEntries, swapEvents } = useEntryActions();
|
||||||
const { selectedEvents, unselect, clearSelectedEvents } = useEventSelection();
|
const { selectedEvents, unselect, clearSelectedEvents } = useEventSelection();
|
||||||
|
|
||||||
const removeOpenEvent = useCallback(() => {
|
const removeOpenEvent = useCallback(() => {
|
||||||
@@ -129,6 +128,13 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
addEntry(newEvent, { after: data.id });
|
addEntry(newEvent, { after: data.id });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'group': {
|
||||||
|
if (selectedEvents.size > 1) {
|
||||||
|
clearMultiSelection();
|
||||||
|
return groupEntries(Array.from(selectedEvents));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'update': {
|
case 'update': {
|
||||||
// Handles and filters update requests
|
// Handles and filters update requests
|
||||||
const { field, value } = payload as FieldValue;
|
const { field, value } = payload as FieldValue;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
IoAdd,
|
IoAdd,
|
||||||
IoDuplicateOutline,
|
IoDuplicateOutline,
|
||||||
|
IoFolder,
|
||||||
IoLink,
|
IoLink,
|
||||||
IoPeople,
|
IoPeople,
|
||||||
IoPeopleOutline,
|
IoPeopleOutline,
|
||||||
@@ -26,7 +27,7 @@ import RundownIndicators from './RundownIndicators';
|
|||||||
import style from './EventBlock.module.scss';
|
import style from './EventBlock.module.scss';
|
||||||
|
|
||||||
interface EventBlockProps {
|
interface EventBlockProps {
|
||||||
eventId: string;
|
eventId: EntryId;
|
||||||
cue: string;
|
cue: string;
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
timeEnd: number;
|
timeEnd: number;
|
||||||
@@ -144,6 +145,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
value: false,
|
value: false,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{ withDivider: true, label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
||||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { isMacOS } from '../../common/utils/deviceUtils';
|
|||||||
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||||
|
|
||||||
interface EventSelectionStore {
|
interface EventSelectionStore {
|
||||||
selectedEvents: Set<string>;
|
selectedEvents: Set<EntryId>;
|
||||||
anchoredIndex: MaybeNumber;
|
anchoredIndex: MaybeNumber;
|
||||||
cursor: MaybeString;
|
cursor: MaybeString;
|
||||||
setSelectedEvents: (selectionArgs: { id: string; index: number; selectMode: SelectionMode }) => void;
|
setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
|
||||||
clearSelectedEvents: () => void;
|
clearSelectedEvents: () => void;
|
||||||
clearMultiSelect: () => void;
|
clearMultiSelect: () => void;
|
||||||
unselect: (id: string) => void;
|
unselect: (id: EntryId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
deleteEvent,
|
deleteEvent,
|
||||||
editEvent,
|
editEvent,
|
||||||
dissolveBlock,
|
dissolveBlock,
|
||||||
|
groupEntries,
|
||||||
reorderEntry,
|
reorderEntry,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
} from '../../services/rundown-service/RundownService.js';
|
} from '../../services/rundown-service/RundownService.js';
|
||||||
@@ -137,6 +138,16 @@ export async function rundownDissolveBlock(req: Request, res: Response<Rundown |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function rundownAddToBlock(req: Request, res: Response<Rundown | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const newRundown = await groupEntries(req.body.ids);
|
||||||
|
res.status(200).send(newRundown);
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(400).send({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function rundownDelete(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
export async function rundownDelete(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
await deleteAllEntries();
|
await deleteAllEntries();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deletesEventById,
|
deletesEventById,
|
||||||
|
rundownAddToBlock,
|
||||||
rundownApplyDelay,
|
rundownApplyDelay,
|
||||||
rundownBatchPut,
|
rundownBatchPut,
|
||||||
rundownDelete,
|
rundownDelete,
|
||||||
@@ -39,6 +40,7 @@ router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
|||||||
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||||
router.post('/dissolve/:eventId', paramsMustHaveEventId, rundownDissolveBlock);
|
router.post('/dissolve/:eventId', paramsMustHaveEventId, rundownDissolveBlock);
|
||||||
|
router.post('/group', rundownArrayOfIds, rundownAddToBlock);
|
||||||
|
|
||||||
router.delete('/', rundownArrayOfIds, deletesEventById);
|
router.delete('/', rundownArrayOfIds, deletesEventById);
|
||||||
router.delete('/all', rundownDelete);
|
router.delete('/all', rundownDelete);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { OntimeEvent, SupportedEntry, TimeStrategy } from 'ontime-types';
|
import { OntimeBlock, OntimeEvent, SupportedEntry, TimeStrategy } from 'ontime-types';
|
||||||
import { generateId, validateEndAction, validateTimerType, validateTimes } from 'ontime-utils';
|
import { generateId, validateEndAction, validateTimerType, validateTimes } from 'ontime-utils';
|
||||||
|
|
||||||
import { event as eventDef } from '../../models/eventsDefinition.js';
|
import { event as eventDef, block as blockDef } from '../../models/eventsDefinition.js';
|
||||||
import { makeString } from '../../utils/parserUtils.js';
|
import { makeString } from '../../utils/parserUtils.js';
|
||||||
|
|
||||||
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
|
export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<OntimeEvent>): OntimeEvent {
|
||||||
@@ -68,6 +68,32 @@ export const createEvent = (eventArgs: Partial<OntimeEvent>, eventIndex: number
|
|||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new block from an optional patch
|
||||||
|
*/
|
||||||
|
export function createBlock(patch?: Partial<OntimeBlock>): OntimeBlock {
|
||||||
|
if (!patch) {
|
||||||
|
return { ...blockDef, id: generateId() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: patch.id ?? generateId(),
|
||||||
|
type: SupportedEntry.Block,
|
||||||
|
title: patch.title ?? '',
|
||||||
|
note: patch.note ?? '',
|
||||||
|
events: patch.events ?? [],
|
||||||
|
skip: patch.skip ?? false,
|
||||||
|
colour: makeString(patch.colour, ''),
|
||||||
|
custom: patch.custom ?? {},
|
||||||
|
revision: 0,
|
||||||
|
startTime: null,
|
||||||
|
endTime: null,
|
||||||
|
duration: 0,
|
||||||
|
isFirstLinked: false,
|
||||||
|
numEvents: patch.events?.length ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function infers strategy for a patch with only partial timer data
|
* Function infers strategy for a patch with only partial timer data
|
||||||
* @param end
|
* @param end
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { getCueCandidate } from 'ontime-utils';
|
import { getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
import { block as blockDef, delay as delayDef } from '../../models/eventsDefinition.js';
|
import { delay as delayDef } from '../../models/eventsDefinition.js';
|
||||||
import { sendRefetch } from '../../adapters/websocketAux.js';
|
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||||
import { createEvent } from '../../api-data/rundown/rundown.utils.js';
|
import { createBlock, createEvent } from '../../api-data/rundown/rundown.utils.js';
|
||||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||||
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
import { runtimeService } from '../runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ function generateEvent<T extends Partial<OntimeEvent> | Partial<OntimeDelay> | P
|
|||||||
|
|
||||||
// TODO(v4): allow user to provide a larger patch of the block entry
|
// TODO(v4): allow user to provide a larger patch of the block entry
|
||||||
if (isOntimeBlock(eventData)) {
|
if (isOntimeBlock(eventData)) {
|
||||||
return { ...blockDef, title: eventData?.title ?? '', id } as CompleteEntry<T>;
|
return createBlock({ id, title: eventData.title ?? '' }) as CompleteEntry<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Invalid event type');
|
throw new Error('Invalid event type');
|
||||||
@@ -235,6 +235,22 @@ export async function dissolveBlock(blockId: EntryId) {
|
|||||||
return newRundown;
|
return newRundown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups a list of entries into a block
|
||||||
|
*/
|
||||||
|
export async function groupEntries(entryIds: EntryId[]) {
|
||||||
|
const scopedMutation = cache.mutateCache(cache.groupEntries);
|
||||||
|
const { newRundown } = await scopedMutation({ entryIds });
|
||||||
|
|
||||||
|
// notify runtime that rundown has changed
|
||||||
|
updateRuntimeOnChange();
|
||||||
|
|
||||||
|
// we dont need to modify the timer since the grouping does not affect the runtime
|
||||||
|
notifyChanges({ external: true });
|
||||||
|
|
||||||
|
return newRundown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* swaps two events
|
* swaps two events
|
||||||
* @param {string} from - id of event from
|
* @param {string} from - id of event from
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
removeCustomField,
|
removeCustomField,
|
||||||
customFieldChangelog,
|
customFieldChangelog,
|
||||||
dissolveBlock,
|
dissolveBlock,
|
||||||
|
groupEntries,
|
||||||
} 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 { ProcessedRundownMetadata } from '../rundownCache.utils.js';
|
||||||
@@ -795,6 +796,39 @@ describe('dissolveBlock() mutation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('groupEntries() mutation', () => {
|
||||||
|
it('groups a list of existing events into a new block', () => {
|
||||||
|
const rundown = makeRundown({
|
||||||
|
order: ['1', '2', '3'],
|
||||||
|
flatOrder: ['1', '2', '3'],
|
||||||
|
entries: {
|
||||||
|
'1': makeOntimeEvent({ id: '1', parent: null }),
|
||||||
|
'2': makeOntimeEvent({ id: '2', parent: null }),
|
||||||
|
'3': makeOntimeEvent({ id: '3', parent: null }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { newRundown } = groupEntries({
|
||||||
|
rundown,
|
||||||
|
entryIds: ['1', '2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockId = newRundown.order[0];
|
||||||
|
expect(blockId).toStrictEqual(expect.any(String));
|
||||||
|
expect(newRundown.order).toStrictEqual([expect.any(String), '3']);
|
||||||
|
expect(newRundown.flatOrder).toStrictEqual([expect.any(String), '1', '2', '3']);
|
||||||
|
expect(newRundown.entries).toMatchObject({
|
||||||
|
[blockId]: {
|
||||||
|
type: SupportedEntry.Block,
|
||||||
|
events: ['1', '2'],
|
||||||
|
},
|
||||||
|
'1': { id: '1', type: SupportedEntry.Event, parent: blockId },
|
||||||
|
'2': { id: '2', type: SupportedEntry.Event, parent: blockId },
|
||||||
|
'3': { id: '3', type: SupportedEntry.Event, parent: null },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('swap() mutation', () => {
|
describe('swap() mutation', () => {
|
||||||
it('should correctly swap data between events', () => {
|
it('should correctly swap data between events', () => {
|
||||||
const rundown = makeRundown({
|
const rundown = makeRundown({
|
||||||
|
|||||||
@@ -11,11 +11,19 @@ import {
|
|||||||
OntimeEntry,
|
OntimeEntry,
|
||||||
Rundown,
|
Rundown,
|
||||||
RundownEntries,
|
RundownEntries,
|
||||||
|
OntimeDelay,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { generateId, insertAtIndex, reorderArray, swapEventData, customFieldLabelToKey } from 'ontime-utils';
|
import {
|
||||||
|
generateId,
|
||||||
|
insertAtIndex,
|
||||||
|
reorderArray,
|
||||||
|
swapEventData,
|
||||||
|
customFieldLabelToKey,
|
||||||
|
mergeAtIndex,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { createPatch } from '../../api-data/rundown/rundown.utils.js';
|
import { createBlock, createPatch } from '../../api-data/rundown/rundown.utils.js';
|
||||||
|
|
||||||
import type { RundownMetadata } from './rundown.types.js';
|
import type { RundownMetadata } from './rundown.types.js';
|
||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
@@ -541,6 +549,59 @@ export function dissolveBlock({ rundown, blockId }: DissolveBlockArgs): Mutating
|
|||||||
return { newRundown: rundown, didMutate: true };
|
return { newRundown: rundown, didMutate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GroupArgs = MutationParams<{ entryIds: EntryId[] }>;
|
||||||
|
/**
|
||||||
|
* Groups a list of entries into a block
|
||||||
|
* It ensures that the entries get reassigned parent and the block gets a list of events
|
||||||
|
* The block will be created at the index of the first event in the order, not at the lowest index
|
||||||
|
* Mutates the given rundown
|
||||||
|
* @throws if any of the entries is a block
|
||||||
|
* @throws if any of the entries is not found
|
||||||
|
*/
|
||||||
|
export function groupEntries({ rundown, entryIds }: GroupArgs): MutatingReturn {
|
||||||
|
const block = createBlock({ id: getUniqueId() });
|
||||||
|
|
||||||
|
const nestedEvents: EntryId[] = [];
|
||||||
|
let firstIndex = -1;
|
||||||
|
for (let i = 0; i < entryIds.length; i++) {
|
||||||
|
const entryId = entryIds[i];
|
||||||
|
const entry = rundown.entries[entryId];
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error('Entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOntimeBlock(entry)) {
|
||||||
|
throw new Error('Cannot group a block');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.parent !== null) {
|
||||||
|
throw new Error('Entry already has a parent');
|
||||||
|
}
|
||||||
|
|
||||||
|
// the block will be created at the first selected event position
|
||||||
|
// note that this is not the lowest index
|
||||||
|
if (firstIndex === -1) {
|
||||||
|
firstIndex = rundown.flatOrder.indexOf(entryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedEvents.push(entryId);
|
||||||
|
entry.parent = block.id;
|
||||||
|
rundown.flatOrder = rundown.flatOrder.filter((id) => id !== entryId);
|
||||||
|
rundown.order = rundown.order.filter((id) => id !== entryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.events = 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 ...
|
||||||
|
rundown.order = insertAtIndex(insertIndex, block.id, rundown.order);
|
||||||
|
/// ... and the nested elements after the block in the flat order
|
||||||
|
rundown.flatOrder = mergeAtIndex(insertIndex, [block.id, ...nestedEvents], rundown.flatOrder);
|
||||||
|
rundown.entries[block.id] = block;
|
||||||
|
|
||||||
|
return { newRundown: rundown, didMutate: true };
|
||||||
|
}
|
||||||
|
|
||||||
type SwapArgs = MutationParams<{ fromId: EntryId; toId: EntryId }>;
|
type SwapArgs = MutationParams<{ fromId: EntryId; toId: EntryId }>;
|
||||||
/**
|
/**
|
||||||
* Swap two entries
|
* Swap two entries
|
||||||
|
|||||||
Reference in New Issue
Block a user