feat: editor layout

This commit is contained in:
Carlos Valente
2026-01-21 19:56:52 +01:00
committed by Carlos Valente
parent 967e92e2c8
commit da02ecd113
77 changed files with 2267 additions and 788 deletions
@@ -1,25 +1,18 @@
import { useCallback } from 'react';
import { type EntryId, type OntimeEntry, type Rundown, isOntimeGroup, SupportedEntry } from 'ontime-types';
import {
getFirstNormal,
getLastNormal,
getNextGroupNormal,
getNextNormal,
getPreviousGroupNormal,
getPreviousNormal,
} from 'ontime-utils';
import { type EntryId, type OntimeEntry, type Rundown, SupportedEntry } from 'ontime-types';
import { getNextGroupNormal, getNextNormal, getPreviousGroupNormal, getPreviousNormal } from 'ontime-utils';
import type { useEntryActions } from '../../../common/hooks/useEntryAction';
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
import { SelectionMode } from '../useEventSelection';
type SelectionMode = 'shift' | 'click' | 'ctrl';
const PAGE_SIZE = 5;
interface UseRundownCommandsOptions {
entries: Rundown['entries'];
order: Rundown['order'];
flatOrder: EntryId[];
entryActions: ReturnType<typeof useEntryActions>;
setSelectedEvents: (selection: { id: EntryId; selectMode: SelectionMode; index: number }) => void;
selectEntry: (selection: { id: EntryId; selectMode: SelectionMode; index: number }) => void;
handleCollapseGroup: (collapsed: boolean, groupId: EntryId) => void;
}
@@ -28,9 +21,9 @@ interface UseRundownCommandsOptions {
*/
export function useRundownCommands({
entries,
order,
flatOrder,
entryActions,
setSelectedEvents,
selectEntry: applySelection,
handleCollapseGroup,
}: UseRundownCommandsOptions) {
const { addEntry, clone, deleteEntry, move, reorderEntry } = entryActions;
@@ -38,13 +31,13 @@ export function useRundownCommands({
const deleteAtCursor = useCallback(
(cursor: string | null) => {
if (!cursor) return;
const { entry, index } = getPreviousNormal(entries, order, cursor);
const { entry, index } = getPreviousNormal(entries, flatOrder, cursor);
deleteEntry([cursor]);
if (entry && index !== null) {
setSelectedEvents({ id: entry.id, selectMode: 'click', index });
applySelection({ id: entry.id, selectMode: 'click', index });
}
},
[entries, order, deleteEntry, setSelectedEvents],
[entries, flatOrder, deleteEntry, applySelection],
);
const insertCopyAtId = useCallback(
@@ -67,7 +60,7 @@ export function useRundownCommands({
if (entryCopyMode === 'cut') {
if (!normalisedAtId) {
const firstId = order[0];
const firstId = flatOrder[0];
if (!firstId || firstId === entryCopyId) {
return;
}
@@ -92,7 +85,7 @@ export function useRundownCommands({
before: above ? normalisedAtId ?? undefined : undefined,
});
},
[entries, order, clone, reorderEntry],
[entries, flatOrder, clone, reorderEntry],
);
/**
@@ -111,70 +104,39 @@ export function useRundownCommands({
const selectGroup = useCallback(
(cursor: EntryId | null, direction: 'up' | 'down') => {
if (order.length < 1) {
if (flatOrder.length < 1) {
return null;
}
let newCursor = cursor;
if (cursor === null) {
// there is no cursor, we select the first or last depending on direction
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
if (isOntimeGroup(selected)) {
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
return selected.id;
}
newCursor = selected?.id ?? null;
}
if (newCursor === null) {
return null;
}
// otherwise we select the next or previous
const selected =
direction === 'up'
? getPreviousGroupNormal(entries, order, newCursor)
: getNextGroupNormal(entries, order, newCursor);
? getPreviousGroupNormal(entries, flatOrder, cursor)
: getNextGroupNormal(entries, flatOrder, cursor);
if (selected.entry !== null && selected.index !== null) {
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
if (selected.entry && selected.index !== null) {
applySelection({ id: selected.entry.id, selectMode: 'click', index: selected.index });
return selected.entry.id;
}
return null;
},
[order, entries, setSelectedEvents],
[flatOrder, entries, applySelection],
);
/**
* TODO: getPreviousNormal and getNextNormal do not work across group boundaries
*/
const selectEntry = useCallback(
(cursor: EntryId | null, direction: 'up' | 'down') => {
if (order.length < 1) {
if (flatOrder.length < 1) {
return null;
}
if (cursor === null) {
// there is no cursor, we select the first or last depending on direction if it exists
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
if (selected !== null) {
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
return selected.id;
}
return null;
}
// otherwise we select the next or previous
const selected =
direction === 'up' ? getPreviousNormal(entries, order, cursor) : getNextNormal(entries, order, cursor);
direction === 'up' ? getPreviousNormal(entries, flatOrder, cursor) : getNextNormal(entries, flatOrder, cursor);
if (selected.entry !== null && selected.index !== null) {
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
if (selected.entry && selected.index !== null) {
applySelection({ id: selected.entry.id, selectMode: 'click', index: selected.index });
return selected.entry.id;
}
return null;
},
[order, entries, setSelectedEvents],
[flatOrder, entries, applySelection],
);
const moveEntry = useCallback(
@@ -204,67 +166,52 @@ export function useRundownCommands({
const selectEdge = useCallback(
(direction: 'top' | 'bottom') => {
if (order.length < 1) {
if (flatOrder.length < 1) {
return null;
}
const selected = direction === 'top' ? getFirstNormal(entries, order) : getLastNormal(entries, order);
if (!selected) {
return null;
}
const index = direction === 'top' ? 0 : flatOrder.length - 1;
const selectedId = flatOrder[index];
if (!selectedId) return null;
const index = order.indexOf(selected.id);
if (index === -1) {
return null;
}
setSelectedEvents({ id: selected.id, selectMode: 'click', index });
return selected.id;
applySelection({ id: selectedId, selectMode: 'click', index });
return selectedId;
},
[entries, order, setSelectedEvents],
[flatOrder, applySelection],
);
const selectPage = useCallback(
(cursor: EntryId | null, direction: 'up' | 'down') => {
if (order.length < 1) {
if (flatOrder.length < 1) {
return null;
}
if (cursor === null) {
const selected = direction === 'down' ? getFirstNormal(entries, order) : getLastNormal(entries, order);
if (!selected) {
return null;
}
const index = order.indexOf(selected.id);
if (index !== -1) {
setSelectedEvents({ id: selected.id, selectMode: 'click', index });
return selected.id;
}
return null;
let currentIndex = cursor ? flatOrder.indexOf(cursor) : -1;
if (currentIndex === -1) {
currentIndex = direction === 'down' ? -1 : flatOrder.length;
}
let nextCursor = cursor;
let target: { entry: OntimeEntry | null; index: number | null } | null = null;
let targetIndex = currentIndex;
let lastValidIndex: number | null = null;
for (let step = 0; step < PAGE_SIZE; step += 1) {
const next =
direction === 'down'
? getNextNormal(entries, order, nextCursor)
: getPreviousNormal(entries, order, nextCursor);
if (next.entry === null || next.index === null) {
targetIndex = direction === 'down' ? targetIndex + 1 : targetIndex - 1;
if (targetIndex < 0 || targetIndex >= flatOrder.length) {
break;
}
target = next;
nextCursor = next.entry.id;
lastValidIndex = targetIndex;
}
if (target?.entry && target.index !== null) {
setSelectedEvents({ id: target.entry.id, selectMode: 'click', index: target.index });
return target.entry.id;
if (lastValidIndex === null) {
return null;
}
return null;
const selectedId = flatOrder[lastValidIndex];
if (!selectedId) return null;
applySelection({ id: selectedId, selectMode: 'click', index: lastValidIndex });
return selectedId;
},
[entries, order, setSelectedEvents],
[flatOrder, applySelection],
);
return {
@@ -11,7 +11,7 @@ interface UseRundownDndOptions {
sortableData: EntryId[];
setSortableData: Dispatch<SetStateAction<EntryId[]>>;
getIsCollapsed: (groupId: EntryId) => boolean;
handleCollapseGroup: (collapsed: boolean, groupId: EntryId) => void;
handleCollapseGroup: (collapsed: boolean, groupId: EntryId | undefined) => void;
entryActions: ReturnType<typeof useEntryActions>;
}
@@ -132,13 +132,11 @@ export function useRundownDnd({
if (event.over?.data.current?.type !== SupportedEntry.Group) {
return;
}
const groupId = event.over?.id as EntryId;
const isCollapsed = getIsCollapsed(groupId);
if (isCollapsed) {
handleCollapseGroup(false, groupId);
}
const groupId = event.over?.id as EntryId | undefined;
handleCollapseGroup(false, groupId);
},
[getIsCollapsed, handleCollapseGroup],
[handleCollapseGroup],
);
return useMemo(