import { MouseEvent, useRef } from 'react'; import { IoChevronDown, IoChevronUp, IoDuplicateOutline, IoFolderOpenOutline, IoReorderTwo, IoTrash, } from 'react-icons/io5'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { EntryId, OntimeGroup } from 'ontime-types'; import { MILLIS_PER_MINUTE, millisToString } from 'ontime-utils'; import IconButton from '../../../common/components/buttons/IconButton'; import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { getOffsetState } from '../../../common/utils/offset'; import { cx, getAccessibleColour, timerPlaceholder } from '../../../common/utils/styleUtils'; import { formatDuration } 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 } = useEntryActions(); const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); const selectedEvents = useEventSelection((state) => state.selectedEvents); const [onContextMenu] = useContextMenu([ { type: 'item', label: 'Clone Group', icon: IoDuplicateOutline, onClick: () => clone(data.id), }, { type: 'item', label: 'Ungroup', icon: IoFolderOpenOutline, onClick: () => ungroup(data.id), disabled: data.entries.length === 0, }, { type: 'divider' }, { type: 'item', label: 'Delete Group', icon: IoTrash, 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 setSingleEntrySelection({ 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 [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), transition, cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'inherit', }; return (
onCollapse(!collapsed, data.id)}> {collapsed ? : }
Start
{millisToString(data.timeStart, { fallback: timerPlaceholder })}
End
{millisToString(data.timeEnd, { fallback: timerPlaceholder })}
Duration
{planOffset === null ? (
{formatDuration(data.duration)}
) : (
{formatDuration(data.duration)} {planOffset}
)}
Entries
{data.entries.length}
); }