feat: multiple selection (#703)

feat: multiple event selection

---------

Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com>
Co-authored-by: Alex <ac@omnivox.dk>
This commit is contained in:
Carlos Valente
2024-01-12 13:07:56 +01:00
committed by GitHub
parent f965db148c
commit 7f63cde7a5
20 changed files with 460 additions and 171 deletions
@@ -3,28 +3,44 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoCopyOutline } from '@react-icons/all-files/io5/IoCopyOutline';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
import { IoPeopleOutline } from '@react-icons/all-files/io5/IoPeopleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { useAppMode } from '../../../common/stores/appModeStore';
import useRundown from '../../../common/hooks-query/useRundown';
import copyToClipboard from '../../../common/utils/copyToClipboard';
import { isMacOS } from '../../../common/utils/deviceUtils';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import type { EventItemActions } from '../RundownEntry';
import { useEventIdSwapping } from '../useEventIdSwapping';
import { EditMode, useEventSelection } from '../useEventSelection';
import EventBlockInner from './EventBlockInner';
import style from './EventBlock.module.scss';
const getEditMode = (event: MouseEvent): EditMode => {
if ((isMacOS() && event.metaKey) || event.ctrlKey) {
return 'ctrl';
}
if (event.shiftKey) {
return 'shift';
}
return 'click';
};
interface EventBlockProps {
cue: string;
timeStart: number;
timeEnd: number;
duration: number;
eventId: string;
eventIndex: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
@@ -61,6 +77,7 @@ export default function EventBlock(props: EventBlockProps) {
timeEnd,
duration,
isPublic = true,
eventIndex,
endAction,
timerType,
title,
@@ -80,37 +97,66 @@ export default function EventBlock(props: EventBlockProps) {
isFirstEvent,
} = props;
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
const moveCursorTo = useAppMode((state) => state.setCursor);
const { selectedEvents, setSelectedEvents } = useEventSelection();
const { data: rundown = [] } = useRundown();
const handleRef = useRef<null | HTMLSpanElement>(null);
const [isVisible, setIsVisible] = useState(false);
const openId = useAppMode((state) => state.editId);
const [onContextMenu] = useContextMenu<HTMLDivElement>([
{ label: `Copy ID: ${eventId}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
{
label: 'Toggle public',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: !isPublic,
}),
},
{
label: 'Add to swap',
icon: IoAdd,
onClick: () => setSelectedEventId(eventId),
withDivider: true,
},
{
label: `Swap this event with ${selectedEventId ?? ''}`,
icon: IoSwapVertical,
onClick: () => {
actionHandler('swap', { field: 'id', value: selectedEventId });
clearSelectedEventId();
},
isDisabled: selectedEventId == null || selectedEventId === eventId,
},
]);
const [onContextMenu] = useContextMenu<HTMLDivElement>(
selectedEvents.size > 1
? [
{
label: 'Visiblity',
group: [
{
label: 'Make public',
icon: IoPeople,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: true,
}),
},
{
label: 'Make private',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: false,
}),
},
],
},
]
: [
{ label: `Copy ID: ${eventId}`, icon: IoCopyOutline, onClick: () => copyToClipboard(eventId) },
{
label: 'Toggle public',
icon: IoPeopleOutline,
onClick: () =>
actionHandler('update', {
field: 'isPublic',
value: !isPublic,
}),
},
{
label: 'Add to swap',
icon: IoAdd,
onClick: () => setSelectedEventId(eventId),
withDivider: true,
},
{
label: `Swap this event with ${selectedEventId ?? ''}`,
icon: IoSwapVertical,
onClick: () => {
actionHandler('swap', { field: 'id', value: selectedEventId });
clearSelectedEventId();
},
isDisabled: selectedEventId == null || selectedEventId === eventId,
},
],
);
const {
isDragging,
@@ -179,12 +225,23 @@ export default function EventBlock(props: EventBlockProps) {
isPast ? style.past : null,
selected ? style.selected : null,
playback ? style[playback] : null,
hasCursor ? style.hasCursor : null,
selectedEvents.has(eventId) ? style.hasCursor : null,
]);
const handleFocusClick = (event: MouseEvent) => {
event.stopPropagation();
moveCursorTo(eventId, true);
// 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;
}
const editMode = getEditMode(event);
return setSelectedEvents({ id: eventId, index: eventIndex, rundown, editMode });
// moveCursorTo(eventId, true);
};
return (
@@ -192,7 +249,7 @@ export default function EventBlock(props: EventBlockProps) {
className={blockClasses}
ref={setNodeRef}
style={dragStyle}
onClick={handleFocusClick}
onMouseDown={handleFocusClick}
onContextMenu={onContextMenu}
id='event-block'
>
@@ -204,11 +261,11 @@ export default function EventBlock(props: EventBlockProps) {
</div>
{isVisible && (
<EventBlockInner
isOpen={openId === eventId}
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
eventId={eventId}
eventIndex={eventIndex}
isPublic={isPublic}
endAction={endAction}
timerType={timerType}
@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { memo, MouseEvent, useCallback, useEffect, useState } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { BiArrowToBottom } from '@react-icons/all-files/bi/BiArrowToBottom';
import { IoArrowDown } from '@react-icons/all-files/io5/IoArrowDown';
@@ -14,11 +14,11 @@ import { EndAction, Playback, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { useAppMode } from '../../../common/stores/appModeStore';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import { EventItemActions } from '../RundownEntry';
import { useEventSelection } from '../useEventSelection';
import BlockActionMenu from './composite/BlockActionMenu';
import EventBlockPlayback from './composite/EventBlockPlayback';
@@ -36,11 +36,11 @@ const tooltipProps = {
};
interface EventBlockInnerProps {
isOpen: boolean;
timeStart: number;
timeEnd: number;
duration: number;
eventId: string;
eventIndex: number;
isPublic: boolean;
endAction: EndAction;
timerType: TimerType;
@@ -60,7 +60,6 @@ interface EventBlockInnerProps {
const EventBlockInner = (props: EventBlockInnerProps) => {
const {
isOpen,
timeStart,
timeEnd,
duration,
@@ -83,19 +82,24 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
} = props;
const [renderInner, setRenderInner] = useState(false);
const setEditId = useAppMode((state) => state.setEditId);
const { clearSelectedEvents, selectedEvents } = useEventSelection();
const isOpen = selectedEvents.size === 1 && selectedEvents.has(eventId);
useEffect(() => {
setRenderInner(true);
}, []);
const toggleOpenEvent = useCallback(() => {
if (isOpen) {
setEditId(null);
} else {
setEditId(eventId);
}
}, [eventId, isOpen, setEditId]);
//TODO: fix this
const toggleOpenEvent = useCallback(
(_event: MouseEvent) => {
// if (isOpen) {
// event.stopPropagation();
// clearSelectedEvents();
// }
},
[clearSelectedEvents, isOpen],
);
const eventIsPlaying = playback === Playback.Play;
const eventIsPaused = playback === Playback.Pause;