chore: rename blocks to groups

This commit is contained in:
Carlos Valente
2025-08-09 06:47:13 +02:00
committed by Carlos Valente
parent 486d89ecf4
commit e5d2457717
83 changed files with 987 additions and 981 deletions
@@ -0,0 +1,93 @@
@use '../blockMixins' as *;
.group {
@include block-styling;
margin-block: 0.5rem;
display: grid;
grid-template-columns: 2rem 1fr;
grid-template-areas: 'binder header';
align-items: center;
&.hasCursor {
outline: 1px solid $block-cursor-color;
}
&.expanded {
margin-block: 0.5rem 0;
border-radius: $block-border-radius $block-border-radius 0 0;
border-bottom: 0.25rem solid color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
}
.binder {
grid-area: binder;
height: 100%;
background-color: var(--user-bg, $gray-1050);
color: $section-white;
font-size: 1rem;
display: grid;
place-content: center;
position: relative;
cursor: pointer;
&:focus {
outline: 1px solid $blue-500;
outline-offset: -1px;
}
}
.header {
grid-area: header;
padding-inline: 0.5rem;
background-color: $block-bg2;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.titleRow {
display: flex;
align-items: center;
gap: 0.5rem;
}
.metaRow {
display: flex;
gap: 3rem;
margin-bottom: 0.25rem;
white-space: nowrap;
}
.metaEntry {
width: 4.5em;
:first-child {
font-size: calc(1rem - 3px);
color: $label-gray;
}
}
}
.strike {
text-decoration: line-through;
}
.over {
color: $playback-over;
}
.under {
color: $playback-under;
}
.drag {
@include drag-style;
position: absolute;
margin-top: 0.25rem;
&.isDragging {
cursor: grabbing;
}
&.notAllowed {
cursor: not-allowed;
}
}
@@ -0,0 +1,175 @@
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 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 } 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) {
const handleRef = useRef<null | HTMLSpanElement>(null);
const { clone, ungroup, deleteEntry } = useEntryActions();
const { selectedEvents, setSingleEntrySelection } = useEventSelection();
const [onContextMenu] = useContextMenu<HTMLDivElement>([
{
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 = 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'];
}
return [offset < 0 ? `-${formatDuration(offset * -1)}` : `+${formatDuration(offset)}`, getOffsetState(offset * -1)];
})();
const dragStyle = {
zIndex: isDragging ? 2 : 'inherit',
transform: CSS.Translate.toString(transform),
transition,
cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'grab',
};
return (
<div
className={cx([style.group, hasCursor && style.hasCursor, !collapsed && style.expanded])}
ref={setNodeRef}
onClick={handleFocusClick}
onContextMenu={onContextMenu}
style={{
...dragStyle,
'--user-bg': data.colour || '#929292',
}}
data-testid='rundown-group'
>
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
<span
className={cx([style.drag, isDragging && style.isDragging, isDragging && !isValidDrop && style.notAllowed])}
ref={handleRef}
{...dragAttributes}
{...dragListeners}
>
<IoReorderTwo />
</span>
</div>
<div className={style.header}>
<div className={style.titleRow}>
<TitleEditor title={data.title} entryId={data.id} placeholder='Group title' />
<IconButton aria-label='Collapse' variant='subtle-white' onClick={() => onCollapse(!collapsed, data.id)}>
{collapsed ? <IoChevronUp /> : <IoChevronDown />}
</IconButton>
</div>
<div className={style.metaRow}>
<div className={style.metaEntry}>
<div>Start</div>
<div>{formatTime(data.timeStart)}</div>
</div>
<div className={style.metaEntry}>
<div>End</div>
<div>{formatTime(data.timeEnd)}</div>
</div>
<div className={style.metaEntry}>
<div>Duration</div>
{planOffset === null ? (
<div className={cx([planOffsetLabel !== null && style[planOffsetLabel]])}>
{formatDuration(data.duration)}
</div>
) : (
<div>
<span className={style.strike}>{formatDuration(data.duration)}</span>
<span className={cx([planOffsetLabel !== null && style[planOffsetLabel]])}>{planOffset}</span>
</div>
)}
</div>
<div className={style.metaEntry}>
<div>Entries</div>
<div>{data.entries.length}</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,10 @@
@use '../blockMixins' as *;
.groupEnd {
cursor: default;
height: 1rem;
background-color: var(--user-bg, $gray-1050);
border-radius: 0 0 $block-border-radius $block-border-radius;
margin-bottom: 0.5rem;
}
@@ -0,0 +1,45 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import style from './RundownGroupEnd.module.scss';
interface RundownGroupEndProps {
id: string;
colour?: string;
}
export default function RundownGroupEnd({ id, colour }: RundownGroupEndProps) {
const {
attributes: dragAttributes,
listeners: dragListeners,
setNodeRef,
transform,
transition,
} = useSortable({
id,
data: {
type: 'end-group',
},
animateLayoutChanges: () => false,
disabled: true, // we do not want to drag end groups
});
const dragStyle = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
className={style.groupEnd}
ref={setNodeRef}
{...dragAttributes}
{...dragListeners}
style={{
...dragStyle,
...(colour ? { '--user-bg': colour } : {}),
}}
tabIndex={-1}
/>
);
}