mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 20:39:18 +00:00
chore: rename blocks to groups
This commit is contained in:
committed by
Carlos Valente
parent
486d89ecf4
commit
e5d2457717
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user