mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-04 23:18:01 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dbe504b23 | |||
| 6d5c35eedc | |||
| d511946e71 | |||
| 3ddf61c651 | |||
| 50ae9640f3 | |||
| 494e2a9aef |
@@ -176,6 +176,13 @@ export async function postCloneEntry(
|
|||||||
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
return axios.post(`${rundownPath}/${rundownId}/clone/${entryId}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request events duration to fit inside the group target
|
||||||
|
*/
|
||||||
|
export async function requestFitGroupTarget(rundownId: RundownId, eventId: EntryId): Promise<AxiosResponse<Rundown>> {
|
||||||
|
return axios.post(`${rundownPath}/${rundownId}/${eventId}/fit-group-duration`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP request for grouping a list of entries into a group
|
* HTTP request for grouping a list of entries into a group
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
requestEventSwap,
|
requestEventSwap,
|
||||||
requestGroupEntries,
|
requestGroupEntries,
|
||||||
requestUngroup,
|
requestUngroup,
|
||||||
|
requestFitGroupTarget,
|
||||||
} from '../api/rundown';
|
} from '../api/rundown';
|
||||||
import { logAxiosError } from '../api/utils';
|
import { logAxiosError } from '../api/utils';
|
||||||
import { useEditorSettings } from '../stores/editorSettings';
|
import { useEditorSettings } from '../stores/editorSettings';
|
||||||
@@ -466,7 +467,22 @@ function useEntryActionsForRundown(scopedRundownId: string | undefined) {
|
|||||||
return previousEnd;
|
return previousEnd;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[getCurrentRundownData, updateEntryMutation, queryClient],
|
[getCurrentRundownData, updateEntryMutation, queryClient, resolveCurrentRundownQueryKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates time of existing event so it satisfies the group target duration
|
||||||
|
* @param eventId {EntryId} - id of the event
|
||||||
|
*/
|
||||||
|
const matchGroupDuration = useCallback(
|
||||||
|
async (eventId: EntryId) => {
|
||||||
|
const rundownId = getCurrentRundownData()?.id;
|
||||||
|
if (!rundownId) {
|
||||||
|
throw new Error('Rundown not initialised');
|
||||||
|
}
|
||||||
|
await requestFitGroupTarget(rundownId, eventId);
|
||||||
|
},
|
||||||
|
[getCurrentRundownData],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1009,6 +1025,7 @@ function useEntryActionsForRundown(scopedRundownId: string | undefined) {
|
|||||||
swapEvents,
|
swapEvents,
|
||||||
updateEntry,
|
updateEntry,
|
||||||
updateTimer,
|
updateTimer,
|
||||||
|
matchGroupDuration,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addEntry,
|
addEntry,
|
||||||
@@ -1026,6 +1043,7 @@ function useEntryActionsForRundown(scopedRundownId: string | undefined) {
|
|||||||
swapEvents,
|
swapEvents,
|
||||||
updateEntry,
|
updateEntry,
|
||||||
updateTimer,
|
updateTimer,
|
||||||
|
matchGroupDuration,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber } from 'ontime-types';
|
||||||
import { IoLockClosed, IoLockOpenOutline } from 'react-icons/io5';
|
import { TbTargetArrow, TbTarget } from 'react-icons/tb';
|
||||||
|
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
@@ -37,7 +37,7 @@ export default function TargetDurationInput({ duration, targetDuration, submitHa
|
|||||||
data-testid='lock__duration'
|
data-testid='lock__duration'
|
||||||
render={<IconButton variant='subtle-white' className={isLocked ? style.active : style.inactive} />}
|
render={<IconButton variant='subtle-white' className={isLocked ? style.active : style.inactive} />}
|
||||||
>
|
>
|
||||||
{isLocked ? <IoLockClosed /> : <IoLockOpenOutline />}
|
{isLocked ? <TbTargetArrow /> : <TbTarget />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TimeInputGroup>
|
</TimeInputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { Day, EndAction, EntryId, Playback, TimeStrategy, TimerType } from 'ontime-types';
|
import { Day, EndAction, EntryId, Maybe, OntimeGroup, Playback, TimeStrategy, TimerType } from 'ontime-types';
|
||||||
import { isPlaybackActive } from 'ontime-utils';
|
import { isPlaybackActive } from 'ontime-utils';
|
||||||
import { MouseEvent, useEffect, useRef } from 'react';
|
import { MouseEvent, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
IoTrash,
|
IoTrash,
|
||||||
IoUnlink,
|
IoUnlink,
|
||||||
} from 'react-icons/io5';
|
} from 'react-icons/io5';
|
||||||
import { TbFlagFilled, TbListNumbers } from 'react-icons/tb';
|
import { TbClockPin, TbFlagFilled, TbListNumbers } from 'react-icons/tb';
|
||||||
|
|
||||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||||
|
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||||
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
|
||||||
@@ -102,7 +103,10 @@ export default function RundownEvent({
|
|||||||
const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId);
|
const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId);
|
||||||
const openRenumberDialog = useRenumberCuesDialogStore((state) => state.onOpen);
|
const openRenumberDialog = useRenumberCuesDialogStore((state) => state.onOpen);
|
||||||
|
|
||||||
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActionsContext();
|
const parentGroup = useEntry(parent) as Maybe<OntimeGroup>;
|
||||||
|
|
||||||
|
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents, matchGroupDuration } =
|
||||||
|
useEntryActionsContext();
|
||||||
|
|
||||||
const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId));
|
const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId));
|
||||||
const unselect = useEventSelection((state) => state.unselect);
|
const unselect = useEventSelection((state) => state.unselect);
|
||||||
@@ -172,6 +176,20 @@ export default function RundownEvent({
|
|||||||
updateEntry({ id: eventId, flag: !flag });
|
updateEntry({ id: eventId, flag: !flag });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Match Group Target Duration',
|
||||||
|
description: 'Change event duration to fill the group target',
|
||||||
|
icon: TbClockPin,
|
||||||
|
onClick: () => {
|
||||||
|
if (!parent) return;
|
||||||
|
matchGroupDuration(eventId);
|
||||||
|
},
|
||||||
|
disabled:
|
||||||
|
!parentGroup ||
|
||||||
|
parentGroup.targetDuration === null ||
|
||||||
|
parentGroup.duration === parentGroup.targetDuration,
|
||||||
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
|
|||||||
@@ -90,7 +90,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lockIcon {
|
.lockIcon {
|
||||||
opacity: 0.6;
|
&.inactive {
|
||||||
|
color: $muted-gray;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
color: $active-indicator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.over {
|
.over {
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { useSortable } from '@dnd-kit/sortable';
|
|||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { EntryId, OntimeGroup } from 'ontime-types';
|
import { EntryId, OntimeGroup } from 'ontime-types';
|
||||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||||
import { MouseEvent, useRef } from 'react';
|
import { MouseEvent, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
IoChevronDown,
|
IoChevronDown,
|
||||||
IoChevronUp,
|
IoChevronUp,
|
||||||
IoDuplicateOutline,
|
IoDuplicateOutline,
|
||||||
IoFolderOpenOutline,
|
IoFolderOpenOutline,
|
||||||
IoLockClosed,
|
|
||||||
IoReorderTwo,
|
IoReorderTwo,
|
||||||
IoTrash,
|
IoTrash,
|
||||||
} from 'react-icons/io5';
|
} from 'react-icons/io5';
|
||||||
|
import { TbTargetArrow, TbClockPin } from 'react-icons/tb';
|
||||||
|
|
||||||
import IconButton from '../../../common/components/buttons/IconButton';
|
import IconButton from '../../../common/components/buttons/IconButton';
|
||||||
import Tag from '../../../common/components/tag/Tag';
|
import Tag from '../../../common/components/tag/Tag';
|
||||||
@@ -40,12 +40,18 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
'use memo';
|
'use memo';
|
||||||
|
|
||||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||||
const { clone, ungroup, deleteEntry } = useEntryActionsContext();
|
const { clone, ungroup, deleteEntry, updateEntry } = useEntryActionsContext();
|
||||||
|
|
||||||
const selectSingleEntry = useEventSelection((state) => state.setSingleEntrySelection);
|
const selectSingleEntry = useEventSelection((state) => state.setSingleEntrySelection);
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
||||||
|
|
||||||
|
const isDurationMatching = data.targetDuration !== null && data.targetDuration === data.duration;
|
||||||
|
|
||||||
|
const matchDuration = useCallback(() => {
|
||||||
|
updateEntry({ id: data.id, targetDuration: data.duration });
|
||||||
|
}, [data.duration, data.id, updateEntry]);
|
||||||
|
|
||||||
const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
|
const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@@ -62,6 +68,15 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
disabled: data.entries.length === 0,
|
disabled: data.entries.length === 0,
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Match Content Duration',
|
||||||
|
icon: TbClockPin,
|
||||||
|
onClick: matchDuration,
|
||||||
|
disabled: isDurationMatching,
|
||||||
|
description: "Change group target duration to match it's contents",
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
label: 'Delete Group',
|
label: 'Delete Group',
|
||||||
@@ -186,7 +201,9 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
<Tag className={style.offsetLabel}>{planOffset}</Tag>
|
<Tag className={style.offsetLabel}>{planOffset}</Tag>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{data.targetDuration !== null && <IoLockClosed className={style.lockIcon} />}
|
{data.targetDuration !== null && (
|
||||||
|
<TbTargetArrow className={cx([style.lockIcon, isDurationMatching ? style.active : style.inactive])} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
TimerType,
|
TimerType,
|
||||||
Trigger,
|
Trigger,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { MILLIS_PER_HOUR, createEvent } from 'ontime-utils';
|
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, createEvent } from 'ontime-utils';
|
||||||
import { assertType } from 'vitest';
|
import { assertType } from 'vitest';
|
||||||
|
|
||||||
import { makeOntimeEvent, makeOntimeGroup, makeOntimeMilestone, makeRundown } from '../__mocks__/rundown.mocks.js';
|
import { makeOntimeEvent, makeOntimeGroup, makeOntimeMilestone, makeRundown } from '../__mocks__/rundown.mocks.js';
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
makeDeepClone,
|
makeDeepClone,
|
||||||
mergeRundownPreservingFields,
|
mergeRundownPreservingFields,
|
||||||
isLoadedPlayable,
|
isLoadedPlayable,
|
||||||
|
eventDurationMatchGroupTarget,
|
||||||
} from '../rundown.utils.js';
|
} from '../rundown.utils.js';
|
||||||
|
|
||||||
describe('test event validator', () => {
|
describe('test event validator', () => {
|
||||||
@@ -610,3 +611,98 @@ describe('isLoadedPlayable()', () => {
|
|||||||
expect(isLoadedPlayable('keynote', rundown)).toBe(false);
|
expect(isLoadedPlayable('keynote', rundown)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('eventDurationMatchGroupTarget()', () => {
|
||||||
|
it('returns unchanged duration when group already matches target', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR,
|
||||||
|
groupDuration: MILLIS_PER_HOUR,
|
||||||
|
eventDuration: MILLIS_PER_MINUTE * 30,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increases event duration when group is shorter than target', () => {
|
||||||
|
// Group is 1h short of target, so event duration increases by 1h
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR * 2, // 2h
|
||||||
|
groupDuration: MILLIS_PER_HOUR, // 1h
|
||||||
|
eventDuration: MILLIS_PER_MINUTE * 30, // 30m
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(MILLIS_PER_HOUR + MILLIS_PER_MINUTE * 30); // 1h30m
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decreases event duration when group is longer than target', () => {
|
||||||
|
// Group is 30m over target, so event duration decreases by 30m
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR, // 1h
|
||||||
|
groupDuration: MILLIS_PER_HOUR + MILLIS_PER_MINUTE * 30, // 1h30m
|
||||||
|
eventDuration: MILLIS_PER_MINUTE * 30, // 30m
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles zero target duration', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: 0,
|
||||||
|
groupDuration: MILLIS_PER_HOUR,
|
||||||
|
eventDuration: MILLIS_PER_HOUR,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles zero group duration', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR,
|
||||||
|
groupDuration: 0,
|
||||||
|
eventDuration: MILLIS_PER_MINUTE * 30,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(MILLIS_PER_HOUR + MILLIS_PER_MINUTE * 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles zero event duration', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR,
|
||||||
|
groupDuration: MILLIS_PER_MINUTE * 30,
|
||||||
|
eventDuration: 0,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(MILLIS_PER_HOUR - MILLIS_PER_MINUTE * 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles all zero values', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: 0,
|
||||||
|
groupDuration: 0,
|
||||||
|
eventDuration: 0,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when result would be negative', () => {
|
||||||
|
// Group exceeds target by 1.5h, event shrinks by 1.5h (exceeds event duration)
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_MINUTE * 30,
|
||||||
|
groupDuration: MILLIS_PER_HOUR * 2,
|
||||||
|
eventDuration: MILLIS_PER_HOUR,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles large durations', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: MILLIS_PER_HOUR * 24, // 24h
|
||||||
|
groupDuration: MILLIS_PER_HOUR * 12, // 12h
|
||||||
|
eventDuration: MILLIS_PER_HOUR, // 1h
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(MILLIS_PER_HOUR * 13); // 13h
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when targetDuration is null', () => {
|
||||||
|
const result = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: null,
|
||||||
|
groupDuration: MILLIS_PER_HOUR,
|
||||||
|
eventDuration: MILLIS_PER_MINUTE * 30,
|
||||||
|
});
|
||||||
|
expect(result).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
reorderEntry,
|
reorderEntry,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
ungroupEntries,
|
ungroupEntries,
|
||||||
|
entryFitGroupDuration,
|
||||||
} from './rundown.service.js';
|
} from './rundown.service.js';
|
||||||
import { normalisedToRundownArray } from './rundown.utils.js';
|
import { normalisedToRundownArray } from './rundown.utils.js';
|
||||||
import {
|
import {
|
||||||
@@ -337,6 +338,23 @@ router.post('/:rundownId/ungroup/:id', paramsWithId, async (req: Request, res: R
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a events duration to fit inside the group target
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
'/:rundownId/:id/fit-group-duration',
|
||||||
|
paramsWithId,
|
||||||
|
async (req: Request, res: Response<Rundown | ErrorResponse>) => {
|
||||||
|
try {
|
||||||
|
const rundown = await entryFitGroupDuration(req.params.rundownId, req.params.id);
|
||||||
|
res.status(200).send(rundown);
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
res.status(400).send({ message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a list of entries by their ID
|
* Deletes a list of entries by their ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
hasChanges,
|
hasChanges,
|
||||||
mergeRundownPreservingFields,
|
mergeRundownPreservingFields,
|
||||||
isLoadedPlayable,
|
isLoadedPlayable,
|
||||||
|
eventDurationMatchGroupTarget,
|
||||||
} from './rundown.utils.js';
|
} from './rundown.utils.js';
|
||||||
import { assertInsertAnchorExists, assertInsertAnchorInOrder, assertSingleInsertAnchor } from './rundown.validation.js';
|
import { assertInsertAnchorExists, assertInsertAnchorInOrder, assertSingleInsertAnchor } from './rundown.validation.js';
|
||||||
|
|
||||||
@@ -447,6 +448,69 @@ export async function cloneEntry(rundownId: string, entryId: EntryId, options: I
|
|||||||
return rundownResult;
|
return rundownResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a events duration to fit inside the group target
|
||||||
|
*/
|
||||||
|
export async function entryFitGroupDuration(rundownId: string, entryId: EntryId): Promise<Rundown> {
|
||||||
|
const { rundown, commit } = createTransaction({ rundownId, mutableRundown: true });
|
||||||
|
|
||||||
|
const entry = rundown.entries[entryId];
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error('Entry not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOntimeEvent(entry)) {
|
||||||
|
throw new Error('Entry must be an event');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parent } = entry;
|
||||||
|
if (!parent) {
|
||||||
|
throw new Error('Entry must be in a group');
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = rundown.entries[parent];
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new Error('Group not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOntimeGroup(group)) {
|
||||||
|
throw new Error('Group is not a group');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDuration = eventDurationMatchGroupTarget({
|
||||||
|
targetDuration: group.targetDuration,
|
||||||
|
groupDuration: group.duration,
|
||||||
|
eventDuration: entry.duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newDuration === null) {
|
||||||
|
throw new Error('Unable to fit a duration');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEnd = entry.timeStart + newDuration;
|
||||||
|
|
||||||
|
rundownMutation.edit(rundown, {
|
||||||
|
id: entryId,
|
||||||
|
duration: newDuration,
|
||||||
|
timeEnd: newEnd,
|
||||||
|
timeStrategy: entry.timeStrategy,
|
||||||
|
});
|
||||||
|
const { rundown: rundownResult, rundownMetadata, revision } = await commit();
|
||||||
|
|
||||||
|
// schedule the side effects
|
||||||
|
setImmediate(() => {
|
||||||
|
// notify runtime that rundown has changed
|
||||||
|
updateRuntimeOnChange(rundownMetadata);
|
||||||
|
|
||||||
|
// we need to notify the timer since we might be changing a running event
|
||||||
|
notifyChanges(rundown.id, rundownMetadata, revision, { external: true, timer: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return rundownResult;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups a list of entries into a new group
|
* Groups a list of entries into a new group
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
EntryCustomFields,
|
EntryCustomFields,
|
||||||
EntryId,
|
EntryId,
|
||||||
ImportedFields,
|
ImportedFields,
|
||||||
|
Maybe,
|
||||||
OntimeBaseEvent,
|
OntimeBaseEvent,
|
||||||
OntimeDelay,
|
OntimeDelay,
|
||||||
OntimeEntry,
|
OntimeEntry,
|
||||||
@@ -601,3 +602,27 @@ export function getIntegerAndFraction(value: string): IncrementNumber {
|
|||||||
precision,
|
precision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts an event's duration to fit inside the group target
|
||||||
|
* @param targetDuration - The desired total duration for the group, or null
|
||||||
|
* @param groupDuration - The current total duration of all events in the group
|
||||||
|
* @param eventDuration - The current duration of the event being adjusted
|
||||||
|
* @returns The adjusted event duration, or null if targetDuration is null or
|
||||||
|
* the result would be negative
|
||||||
|
*/
|
||||||
|
export function eventDurationMatchGroupTarget({
|
||||||
|
targetDuration,
|
||||||
|
groupDuration,
|
||||||
|
eventDuration,
|
||||||
|
}: {
|
||||||
|
targetDuration: Maybe<number>;
|
||||||
|
groupDuration: number;
|
||||||
|
eventDuration: number;
|
||||||
|
}): Maybe<number> {
|
||||||
|
if (targetDuration === null) return null;
|
||||||
|
if (targetDuration === groupDuration) return null;
|
||||||
|
const durationDiff = targetDuration - groupDuration;
|
||||||
|
const newDuration = eventDuration + durationDiff;
|
||||||
|
return newDuration < 0 ? null : newDuration;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user