Group duration context menu utils (#1748)

This commit is contained in:
Alex Christoffer Rasmussen
2026-08-09 16:45:47 +02:00
committed by GitHub
parent ac0ef06459
commit a006331fea
10 changed files with 338 additions and 63 deletions
@@ -74,42 +74,36 @@
.metaLabel {
color: $muted-gray;
font-size: calc(1rem - 3px);
display: flex;
align-items: center;
gap: 0.25rem;
}
}
.strike {
text-decoration: wavy underline;
margin-right: 0.25rem;
color: $ui-white;
}
.duration {
display: flex;
align-items: center;
gap: 0.25rem;
color: $ui-white;
&.warning {
.strike {
// color: $playback-over;
text-decoration: wavy underline;
text-decoration-color: $playback-over;
}
.offsetLabel {
background-color: $playback-over;
}
}
}
.lockIcon {
opacity: 0.6;
color: $muted-gray;
}
.over {
color: $playback-over;
.strike {
text-decoration-color: $playback-over;
}
.offsetLabel {
background-color: $playback-over;
}
}
.under {
color: $playback-under;
.strike {
text-decoration-color: $playback-under;
}
.offsetLabel {
background-color: $playback-under;
}
.target {
display: contents;
}
.drag {
@@ -2,24 +2,25 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { EntryId, OntimeGroup } from 'ontime-types';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { MouseEvent, useRef } from 'react';
import { MouseEvent, useCallback, useRef } from 'react';
import {
IoChevronDown,
IoChevronUp,
IoDuplicateOutline,
IoFolderOpenOutline,
IoLockClosed,
IoReorderTwo,
IoTrash,
IoLockClosed,
} from 'react-icons/io5';
import { TbClockPin } from 'react-icons/tb';
import IconButton from '../../../common/components/buttons/IconButton';
import Tag from '../../../common/components/tag/Tag';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
import { deviceAlt, deviceMod } from '../../../common/utils/deviceUtils';
import { getOffsetState } from '../../../common/utils/offset';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import { formatDuration, formatTime } from '../../../common/utils/time';
import TitleEditor from '../common/TitleEditor';
@@ -40,12 +41,31 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
'use memo';
const handleRef = useRef<null | HTMLSpanElement>(null);
const { clone, ungroup, deleteEntry } = useEntryActionsContext();
const { clone, ungroup, deleteEntry, updateEntry } = useEntryActionsContext();
const selectSingleEntry = useEventSelection((state) => state.setSingleEntrySelection);
const selectedEvents = useEventSelection((state) => state.selectedEvents);
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
const isDurationMatching = data.targetDuration !== null && data.targetDuration === data.duration;
const [planOffset, offset] = (() => {
if (data.targetDuration === null) {
return [null, 0];
}
const offset = data.duration - data.targetDuration;
if (offset === 0) {
return [null, 0];
}
const absOffset = Math.abs(offset);
return [`${offset < 0 ? '-' : '+'}${formatDuration(absOffset, absOffset > 2 * MILLIS_PER_MINUTE)}`, offset];
})();
const matchDuration = useCallback(() => {
updateEntry({ id: data.id, targetDuration: data.duration });
}, [data.duration, data.id, updateEntry]);
const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
{
type: 'item',
@@ -62,6 +82,18 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
disabled: data.entries.length === 0,
},
{ type: 'divider' },
{
type: 'item',
label: 'Match Content Duration',
icon: TbClockPin,
onClick: matchDuration,
disabled: isDurationMatching,
description:
offset > 0
? "Increase group target duration to match it's contents"
: "Decrease group target duration to match it's contents",
},
{ type: 'divider' },
{
type: 'item',
label: 'Delete Group',
@@ -105,22 +137,6 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
const binderColours = data.colour && getAccessibleColour(data.colour);
const isValidDrop = isDragging && over?.id && canDrop(over.data.current?.type, over.data.current?.parent);
const [planOffset, planOffsetLabel] = (() => {
if (data.targetDuration === null) {
return [null, null];
}
const offset = data.duration - data.targetDuration;
if (offset === 0) {
return [null, 'under'];
}
const absOffset = Math.abs(offset);
return [
`${offset < 0 ? '-' : '+'}${formatDuration(absOffset, absOffset > 2 * MILLIS_PER_MINUTE)}`,
getOffsetState(offset),
];
})();
const dragStyle = {
zIndex: isDragging ? 2 : 'inherit',
transform: CSS.Translate.toString(transform),
@@ -175,20 +191,18 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
<div className={style.metaLabel}>End</div>
<div>{formatTime(data.timeEnd)}</div>
</div>
<div className={style.metaEntry}>
<div className={style.metaLabel}>Duration</div>
<div className={style.duration}>
{planOffset === null ? (
formatDuration(data.duration)
) : (
<span className={cx([planOffsetLabel && style[planOffsetLabel]])}>
<span className={style.strike}>{formatDuration(data.duration)}</span>
<Tag className={style.offsetLabel}>{planOffset}</Tag>
</span>
)}
{data.targetDuration !== null && <IoLockClosed className={style.lockIcon} />}
<Tooltip text={'Group has target duration'} disabled={data.targetDuration === null}>
<div className={style.metaEntry}>
<div className={style.metaLabel}>
Duration
{data.targetDuration !== null && <IoLockClosed className={style.lockIcon} />}
</div>
<div className={cx([style.duration, planOffset && style.warning])}>
<span className={style.strike}>{formatDuration(data.duration)}</span>
{planOffset && <Tag className={style.offsetLabel}>{planOffset}</Tag>}
</div>
</div>
</div>
</Tooltip>
</div>
</div>
</div>