diff --git a/apps/client/src/common/api/rundown.ts b/apps/client/src/common/api/rundown.ts index 1a6e63419..80c497ca8 100644 --- a/apps/client/src/common/api/rundown.ts +++ b/apps/client/src/common/api/rundown.ts @@ -97,8 +97,8 @@ export async function postCloneEntry(entryId: EntryId): Promise> { - return axios.post(`${rundownPath}/dissolve/${blockId}`); +export async function requestUngroup(blockId: EntryId): Promise> { + return axios.post(`${rundownPath}/ungroup/${blockId}`); } /** diff --git a/apps/client/src/common/hooks/useEntryAction.ts b/apps/client/src/common/hooks/useEntryAction.ts index 85d28b345..0051056ca 100644 --- a/apps/client/src/common/hooks/useEntryAction.ts +++ b/apps/client/src/common/hooks/useEntryAction.ts @@ -26,9 +26,9 @@ import { ReorderEntry, requestApplyDelay, requestDeleteAll, - requestDissolveBlock, requestEventSwap, requestGroupEntries, + requestUngroup, SwapEntry, } from '../api/rundown'; import { logAxiosError } from '../api/utils'; @@ -544,8 +544,8 @@ export const useEntryActions = () => { * Calls mutation to dissolve a block * @private */ - const _dissolveBlockMutation = useMutation({ - mutationFn: requestDissolveBlock, + const _ungroupMutation = useMutation({ + mutationFn: requestUngroup, onSuccess: (response) => { if (!response.data) return; @@ -565,15 +565,15 @@ export const useEntryActions = () => { /** * Deletes a block and moves its events to the top level */ - const dissolveBlock = useCallback( + const ungroup = useCallback( async (blockId: EntryId) => { try { - await _dissolveBlockMutation.mutateAsync(blockId); + await _ungroupMutation.mutateAsync(blockId); } catch (error) { logAxiosError('Error dissolving block', error); } }, - [_dissolveBlockMutation], + [_ungroupMutation], ); /** @@ -762,7 +762,7 @@ export const useEntryActions = () => { clone, deleteEntry, deleteAllEntries, - dissolveBlock, + ungroup, getEntryById, groupEntries, reorderEntry, diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 704353456..6a4e1cc73 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -78,7 +78,7 @@ export default function Rundown({ data }: RundownProps) { useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: appMode === AppMode.Run }); // DND KIT - const sensors = useSensors(useSensor(PointerSensor)); + const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } })); const deleteAtCursor = useCallback( (cursor: string | null) => { diff --git a/apps/client/src/features/rundown/block-block/BlockBlock.tsx b/apps/client/src/features/rundown/block-block/BlockBlock.tsx index 40f88d5a7..7690cdef8 100644 --- a/apps/client/src/features/rundown/block-block/BlockBlock.tsx +++ b/apps/client/src/features/rundown/block-block/BlockBlock.tsx @@ -3,16 +3,16 @@ import { IoChevronDown, IoChevronUp, IoDuplicateOutline, - IoEllipsisHorizontal, IoFolderOpenOutline, IoReorderTwo, IoTrash, } from 'react-icons/io5'; -import { IconButton, Menu, MenuButton, MenuItem, MenuList, Portal } from '@chakra-ui/react'; +import { IconButton } from '@chakra-ui/react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { EntryId, OntimeBlock } from 'ontime-types'; +import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import { formatDuration, formatTime } from '../../../common/utils/time'; @@ -31,7 +31,27 @@ interface BlockBlockProps { export default function BlockBlock(props: BlockBlockProps) { const { data, hasCursor, collapsed, onCollapse } = props; const handleRef = useRef(null); - const { clone, dissolveBlock, deleteEntry } = useEntryActions(); + const { clone, ungroup, deleteEntry } = useEntryActions(); + + const [onContextMenu] = useContextMenu([ + { + label: 'Clone Block', + icon: IoDuplicateOutline, + onClick: () => clone(data.id), + }, + { + label: 'Ungroup', + icon: IoFolderOpenOutline, + onClick: () => ungroup(data.id), + isDisabled: data.events.length === 0, + }, + { + label: 'Delete Block', + icon: IoTrash, + onClick: () => deleteEntry([data.id]), + withDivider: true, + }, + ]); const { attributes: dragAttributes, @@ -60,12 +80,11 @@ export default function BlockBlock(props: BlockBlockProps) { cursor: isOver ? (isValidDrop ? 'grabbing' : 'no-drop') : 'default', }; - const hasChildren = data.events.length > 0; - return (
- - } - color='#e2e2e2' // $gray-200 - variant='ontime-ghosted' - size='sm' - /> - - - } onClick={() => clone(data.id)}> - Clone Block - - {hasChildren && ( - } onClick={() => dissolveBlock(data.id)}> - Dissolve Block - - )} - } onClick={() => deleteEntry([data.id])}> - Delete Block - - - - onCollapse(!collapsed, data.id)} diff --git a/apps/server/src/api-data/rundown/rundown.controller.ts b/apps/server/src/api-data/rundown/rundown.controller.ts index ac743bbe3..ce84fdfcf 100644 --- a/apps/server/src/api-data/rundown/rundown.controller.ts +++ b/apps/server/src/api-data/rundown/rundown.controller.ts @@ -11,7 +11,7 @@ import { deleteAllEntries, deleteEvent, editEvent, - dissolveBlock, + ungroupEntries, groupEntries, reorderEntry, swapEvents, @@ -139,9 +139,9 @@ export async function rundownCloneEntry(req: Request, res: Response) { +export async function rundownUngroupEntries(req: Request, res: Response) { try { - const newRundown = await dissolveBlock(req.params.entryId); + const newRundown = await ungroupEntries(req.params.entryId); res.status(200).send(newRundown); } catch (error) { const message = getErrorMessage(error); diff --git a/apps/server/src/api-data/rundown/rundown.router.ts b/apps/server/src/api-data/rundown/rundown.router.ts index 44e1bcc97..e2cb3ccf9 100644 --- a/apps/server/src/api-data/rundown/rundown.router.ts +++ b/apps/server/src/api-data/rundown/rundown.router.ts @@ -7,7 +7,7 @@ import { rundownBatchPut, rundownCloneEntry, rundownDelete, - rundownDissolveBlock, + rundownUngroupEntries, rundownGetAll, rundownGetById, rundownGetCurrent, @@ -41,7 +41,7 @@ router.patch('/reorder/', rundownReorderValidator, rundownReorder); router.patch('/swap', rundownSwapValidator, rundownSwap); router.patch('/applydelay/:entryId', paramsMustHaveEntryId, rundownApplyDelay); router.post('/clone/:entryId', paramsMustHaveEntryId, rundownCloneEntry); -router.post('/dissolve/:entryId', paramsMustHaveEntryId, rundownDissolveBlock); +router.post('/ungroup/:entryId', paramsMustHaveEntryId, rundownUngroupEntries); router.post('/group', rundownArrayOfIds, rundownAddToBlock); router.delete('/', rundownArrayOfIds, deletesEventById); diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index 413571b5c..2bc5b90b0 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -244,8 +244,8 @@ export async function cloneEntry(entryId: EntryId) { /** * Deletes a block from the rundown and moves all its children to the top level */ -export async function dissolveBlock(blockId: EntryId) { - const scopedMutation = cache.mutateCache(cache.dissolveBlock); +export async function ungroupEntries(blockId: EntryId) { + const scopedMutation = cache.mutateCache(cache.ungroup); const { newRundown } = await scopedMutation({ blockId }); // notify runtime that rundown has changed diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index 44d817a0a..cc78148f9 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -15,7 +15,7 @@ import { editCustomField, removeCustomField, customFieldChangelog, - dissolveBlock, + ungroup, groupEntries, clone, } from '../rundownCache.js'; @@ -870,7 +870,7 @@ describe('clone() mutation', () => { }); }); -describe('dissolveBlock() mutation', () => { +describe('ungroup() mutation', () => { it('should correctly dissolve a block into its events', () => { const rundown = makeRundown({ order: ['1', '2'], @@ -883,7 +883,7 @@ describe('dissolveBlock() mutation', () => { }, }); - const { newRundown } = dissolveBlock({ + const { newRundown } = ungroup({ rundown, blockId: '2', }); diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 2331e13d8..439ee2fee 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -594,7 +594,7 @@ export function clone({ rundown, entryId }: CloneEntryArgs): MutatingReturn { const flatIndex = rundown.flatOrder.indexOf(lastNestedIdInOriginal) + 1; newBlock.events = nestedIds; - newBlock.title = `${entry.title} (copy)`; + newBlock.title = `${entry.title || 'Untitled'} (copy)`; rundown.entries[newBlock.id] = newBlock; rundown.order = insertAtIndex(atIndex, newBlock.id, rundown.order); @@ -606,13 +606,13 @@ export function clone({ rundown, entryId }: CloneEntryArgs): MutatingReturn { } } -type DissolveBlockArgs = MutationParams<{ blockId: EntryId }>; +type UngroupArgs = MutationParams<{ blockId: EntryId }>; /** * Deletes a block and moves all its children to the top level order * Mutates the given rundown * @throws if block ID not found */ -export function dissolveBlock({ rundown, blockId }: DissolveBlockArgs): MutatingReturn { +export function ungroup({ rundown, blockId }: UngroupArgs): MutatingReturn { const block = rundown.entries[blockId]; if (!isOntimeBlock(block)) { throw new Error('Block with ID not found');