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, useCallback, useRef } from 'react'; import { IoChevronDown, IoChevronUp, IoDuplicateOutline, IoFolderOpenOutline, 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 { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../../common/utils/time'; import TitleEditor from '../common/TitleEditor'; import { canDrop } from '../rundown.utils'; import { useEventSelection } from '../useEventSelection'; import style from './RundownGroup.module.scss'; interface RundownGroupProps { data: OntimeGroup; hasCursor: boolean; collapsed: boolean; onCollapse: (collapsed: boolean, groupId: EntryId) => void; } //TODO: the group should maybe include a multiple day indicator export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }: RundownGroupProps) { 'use memo'; const handleRef = useRef(null); 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(() => [ { type: 'item', label: 'Clone Group', icon: IoDuplicateOutline, shortcut: `${deviceMod}+D`, onClick: () => clone(data.id), }, { type: 'item', label: 'Ungroup', icon: IoFolderOpenOutline, onClick: () => ungroup(data.id), 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', icon: IoTrash, shortcut: `${deviceAlt}+Backspace`, onClick: () => deleteEntry([data.id]), }, ]); const { attributes: dragAttributes, listeners: dragListeners, setNodeRef, transform, transition, isDragging, isOver, over, } = useSortable({ id: data.id, data: { type: 'group', }, animateLayoutChanges: () => false, }); const handleFocusClick = (event: MouseEvent) => { event.stopPropagation(); // event.button === 2 is a right-click // disable selection if the user selected events and right clicks // so the context menu shows up if (selectedEvents.size > 1 && event.button === 2) { return; } // UI indexes are 1 based selectSingleEntry({ id: data.id }); }; const binderColours = data.colour && getAccessibleColour(data.colour); const isValidDrop = isDragging && over?.id && canDrop(over.data.current?.type, over.data.current?.parent); const dragStyle = { zIndex: isDragging ? 2 : 'inherit', transform: CSS.Translate.toString(transform), transition, cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'inherit', }; return (
onCollapse(!collapsed, data.id)}> {collapsed ? : }
Entries
{data.entries.length}
Start
{formatTime(data.timeStart)}
End
{formatTime(data.timeEnd)}
Duration {data.targetDuration !== null && }
{formatDuration(data.duration)} {planOffset && {planOffset}}
); }