mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
feat: extend rundown shortcuts
This commit is contained in:
committed by
Carlos Valente
parent
d7af73d9ed
commit
967e92e2c8
@@ -31,10 +31,11 @@
|
||||
outline: 0;
|
||||
cursor: default;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem 2rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
line-height: 1em;
|
||||
|
||||
svg {
|
||||
@@ -69,6 +70,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
color: $gray-500;
|
||||
font-size: calc(1rem - 4px);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0.25rem 0.75rem;
|
||||
height: 1px;
|
||||
|
||||
@@ -11,6 +11,7 @@ type DropdownMenuItem = {
|
||||
icon?: IconType;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
shortcut?: string;
|
||||
};
|
||||
|
||||
export type DropdownMenuOption = DropdownMenuItemDivider | DropdownMenuItem;
|
||||
@@ -38,8 +39,11 @@ export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChil
|
||||
disabled={item.disabled}
|
||||
data-type={item.type}
|
||||
>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
<span className={style.label}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</span>
|
||||
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||
</BaseMenu.Item>
|
||||
);
|
||||
})}
|
||||
@@ -75,8 +79,11 @@ export function PositionedDropdownMenu({ items, isOpen, position, onClose }: Pos
|
||||
}
|
||||
return (
|
||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
<span className={style.label}>
|
||||
{item.icon && <item.icon />}
|
||||
{item.label}
|
||||
</span>
|
||||
{item.shortcut && <span className={style.shortcut}>{item.shortcut}</span>}
|
||||
</BaseMenu.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,10 +2,13 @@ import { create } from 'zustand';
|
||||
|
||||
type EntryCopyStore = {
|
||||
entryCopyId: string | null;
|
||||
setEntryCopyId: (eventId: string | null) => void;
|
||||
entryCopyMode: 'copy' | 'cut';
|
||||
setEntryCopyId: (eventId: string | null, mode?: 'copy' | 'cut') => void;
|
||||
};
|
||||
|
||||
export const useEntryCopy = create<EntryCopyStore>()((set) => ({
|
||||
entryCopyId: null,
|
||||
setEntryCopyId: (entryCopyId: string | null) => set({ entryCopyId }),
|
||||
entryCopyMode: 'copy',
|
||||
setEntryCopyId: (entryCopyId: string | null, mode: 'copy' | 'cut' = 'copy') =>
|
||||
set({ entryCopyId, entryCopyMode: mode }),
|
||||
}));
|
||||
|
||||
@@ -30,6 +30,7 @@ interface RundownProps {
|
||||
entries: Rundown['entries'];
|
||||
id: Rundown['id'];
|
||||
order: Rundown['order'];
|
||||
flatOrder: Rundown['flatOrder'];
|
||||
rundownMetadata: RundownMetadataObject;
|
||||
featureData: {
|
||||
playback: Playback;
|
||||
@@ -38,7 +39,7 @@ interface RundownProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default function Rundown({ order, entries, id, rundownMetadata, featureData }: RundownProps) {
|
||||
export default function Rundown({ order, flatOrder, entries, id, rundownMetadata, featureData }: RundownProps) {
|
||||
// invoke the compiler for the component
|
||||
'use memo';
|
||||
|
||||
@@ -73,6 +74,8 @@ export default function Rundown({ order, entries, id, rundownMetadata, featureDa
|
||||
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||
const cursor = useEventSelection((state) => state.cursor);
|
||||
const scrollToEntry = useEventSelection((state) => state.scrollToEntry);
|
||||
const setScrollHandler = useEventSelection((state) => state.setScrollHandler);
|
||||
|
||||
const cursorRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -111,7 +114,7 @@ export default function Rundown({ order, entries, id, rundownMetadata, featureDa
|
||||
// Commands layer - business logic
|
||||
const commands = useRundownCommands({
|
||||
entries,
|
||||
order,
|
||||
flatOrder,
|
||||
entryActions,
|
||||
setSelectedEvents,
|
||||
handleCollapseGroup,
|
||||
@@ -142,41 +145,42 @@ export default function Rundown({ order, entries, id, rundownMetadata, featureDa
|
||||
return filterVisibleEntries(sortableData, entries, getIsCollapsed);
|
||||
}, [sortableData, entries, getIsCollapsed]);
|
||||
|
||||
// Follow-scroll with Virtuoso in run mode
|
||||
// Always scrolls when playback selection changes, not during drag operations
|
||||
// Scroll to a specific entry when requested by keyboard/finder
|
||||
useEffect(() => {
|
||||
if (editorMode !== AppMode.Run || !virtuosoRef.current || dnd.isDraggingRef.current) return;
|
||||
setScrollHandler('rundown-list', (entryId) => {
|
||||
if (!virtuosoRef.current || dnd.isDraggingRef.current) {
|
||||
return;
|
||||
}
|
||||
const index = visibleData.indexOf(entryId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
offset: -100, // show the previous entry for context
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
setScrollHandler('rundown-list', null);
|
||||
};
|
||||
}, [visibleData, dnd.isDraggingRef, setScrollHandler]);
|
||||
|
||||
// Follow-scroll in run mode via the shared scroll handler
|
||||
useEffect(() => {
|
||||
if (editorMode !== AppMode.Run || dnd.isDraggingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = featureData?.selectedEventId;
|
||||
if (!targetId) return;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = visibleData.indexOf(targetId);
|
||||
if (index === -1) return;
|
||||
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
offset: -100,
|
||||
});
|
||||
}, [editorMode, featureData?.selectedEventId, visibleData, dnd.isDraggingRef]);
|
||||
|
||||
// Scroll to the active cursor when editing (e.g. finder results)
|
||||
useEffect(() => {
|
||||
if (editorMode !== AppMode.Edit || !virtuosoRef.current || dnd.isDraggingRef.current) return;
|
||||
|
||||
if (!cursor) return;
|
||||
|
||||
const index = visibleData.indexOf(cursor);
|
||||
if (index === -1) return;
|
||||
|
||||
virtuosoRef.current.scrollToIndex({
|
||||
index,
|
||||
align: 'start',
|
||||
behavior: 'smooth',
|
||||
offset: -100, // show the previous entry for context
|
||||
});
|
||||
}, [editorMode, cursor, visibleData, dnd.isDraggingRef]);
|
||||
scrollToEntry(targetId);
|
||||
}, [editorMode, featureData?.selectedEventId, visibleData, dnd.isDraggingRef, scrollToEntry]);
|
||||
|
||||
// in run mode, we follow the playback selection and open groups as needed
|
||||
useEffect(() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ function RundownList() {
|
||||
return (
|
||||
<Rundown
|
||||
order={data.order}
|
||||
flatOrder={data.flatOrder}
|
||||
entries={data.entries}
|
||||
id={data.id}
|
||||
rundownMetadata={rundownMetadata}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
.shortcutSection {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
margin-top: 15vh;
|
||||
margin-inline: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
border-spacing: 4rem 0;
|
||||
|
||||
tr {
|
||||
white-space: nowrap;
|
||||
td:nth-child(odd) {
|
||||
text-align: left;
|
||||
}
|
||||
td:nth-child(even) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,22 @@ function EventEditorEmpty() {
|
||||
<Kbd>↓</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jump to top / bottom</td>
|
||||
<td>
|
||||
<Kbd>Home</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>End</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Page up / down</td>
|
||||
<td>
|
||||
<Kbd>PgUp</Kbd>
|
||||
<AuxKey>/</AuxKey>
|
||||
<Kbd>PgDn</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deselect entry</td>
|
||||
<td>
|
||||
@@ -80,6 +96,14 @@ function EventEditorEmpty() {
|
||||
<Kbd>C</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cut selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>X</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Paste above</td>
|
||||
<td>
|
||||
@@ -99,10 +123,18 @@ function EventEditorEmpty() {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete selected entry</td>
|
||||
<td>Clone selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceMod}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>D</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete selected entry</td>
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Backspace</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -140,7 +172,7 @@ function EventEditorEmpty() {
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>Shift</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>M</Kbd>
|
||||
<Kbd>G</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -148,7 +180,7 @@ function EventEditorEmpty() {
|
||||
<td>
|
||||
<Kbd>{deviceAlt}</Kbd>
|
||||
<AuxKey>+</AuxKey>
|
||||
<Kbd>G</Kbd>
|
||||
<Kbd>M</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
|
||||
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
interface UseRundownCommandsOptions {
|
||||
entries: Rundown['entries'];
|
||||
@@ -22,6 +23,9 @@ interface UseRundownCommandsOptions {
|
||||
handleCollapseGroup: (collapsed: boolean, groupId: EntryId) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common operations for the rundown lists
|
||||
*/
|
||||
export function useRundownCommands({
|
||||
entries,
|
||||
order,
|
||||
@@ -29,7 +33,8 @@ export function useRundownCommands({
|
||||
setSelectedEvents,
|
||||
handleCollapseGroup,
|
||||
}: UseRundownCommandsOptions) {
|
||||
const { addEntry, clone, deleteEntry, move } = entryActions;
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = entryActions;
|
||||
|
||||
const deleteAtCursor = useCallback(
|
||||
(cursor: string | null) => {
|
||||
if (!cursor) return;
|
||||
@@ -45,7 +50,7 @@ export function useRundownCommands({
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: EntryId | null, above = false) => {
|
||||
// lazily get the value from the store
|
||||
const { entryCopyId } = useEntryCopy.getState();
|
||||
const { entryCopyId, entryCopyMode, setEntryCopyId } = useEntryCopy.getState();
|
||||
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
@@ -60,13 +65,34 @@ export function useRundownCommands({
|
||||
normalisedAtId = refElement.parent;
|
||||
}
|
||||
|
||||
if (entryCopyMode === 'cut') {
|
||||
if (!normalisedAtId) {
|
||||
const firstId = order[0];
|
||||
if (!firstId || firstId === entryCopyId) {
|
||||
return;
|
||||
}
|
||||
reorderEntry(entryCopyId, firstId, 'before')
|
||||
.then(() => setEntryCopyId(null))
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (normalisedAtId === entryCopyId) {
|
||||
return;
|
||||
}
|
||||
const placement = above ? 'before' : 'after';
|
||||
reorderEntry(entryCopyId, normalisedAtId, placement)
|
||||
.then(() => setEntryCopyId(null))
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
clone(entryCopyId, {
|
||||
after: above ? undefined : normalisedAtId ?? undefined,
|
||||
// if we don't have a cursor add the new event on top
|
||||
before: above ? normalisedAtId ?? undefined : undefined,
|
||||
});
|
||||
},
|
||||
[entries, clone],
|
||||
[entries, order, clone, reorderEntry],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -86,7 +112,7 @@ export function useRundownCommands({
|
||||
const selectGroup = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
let newCursor = cursor;
|
||||
if (cursor === null) {
|
||||
@@ -95,13 +121,13 @@ export function useRundownCommands({
|
||||
|
||||
if (isOntimeGroup(selected)) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
return;
|
||||
return selected.id;
|
||||
}
|
||||
newCursor = selected?.id ?? null;
|
||||
}
|
||||
|
||||
if (newCursor === null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// otherwise we select the next or previous
|
||||
@@ -112,7 +138,9 @@ export function useRundownCommands({
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
return selected.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
@@ -123,7 +151,7 @@ export function useRundownCommands({
|
||||
const selectEntry = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cursor === null) {
|
||||
@@ -131,8 +159,9 @@ export function useRundownCommands({
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
// otherwise we select the next or previous
|
||||
@@ -141,7 +170,9 @@ export function useRundownCommands({
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
return selected.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
@@ -161,12 +192,90 @@ export function useRundownCommands({
|
||||
[handleCollapseGroup, move],
|
||||
);
|
||||
|
||||
const cloneEntry = useCallback(
|
||||
(cursor: EntryId | null) => {
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
clone(cursor, { after: cursor });
|
||||
},
|
||||
[clone],
|
||||
);
|
||||
|
||||
const selectEdge = useCallback(
|
||||
(direction: 'top' | 'bottom') => {
|
||||
if (order.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selected = direction === 'top' ? getFirstNormal(entries, order) : getLastNormal(entries, order);
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = order.indexOf(selected.id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index });
|
||||
return selected.id;
|
||||
},
|
||||
[entries, order, setSelectedEvents],
|
||||
);
|
||||
|
||||
const selectPage = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.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 nextCursor = cursor;
|
||||
let target: { entry: OntimeEntry | null; index: number | null } | 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) {
|
||||
break;
|
||||
}
|
||||
target = next;
|
||||
nextCursor = next.entry.id;
|
||||
}
|
||||
|
||||
if (target?.entry && target.index !== null) {
|
||||
setSelectedEvents({ id: target.entry.id, selectMode: 'click', index: target.index });
|
||||
return target.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[entries, order, setSelectedEvents],
|
||||
);
|
||||
|
||||
return {
|
||||
cloneEntry,
|
||||
deleteAtCursor,
|
||||
insertCopyAtId,
|
||||
insertAtId,
|
||||
selectGroup,
|
||||
selectEntry,
|
||||
moveEntry,
|
||||
selectEdge,
|
||||
selectPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { type OntimeEntry, EntryId, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
interface UseRundownKeyboardOptions {
|
||||
cursor: EntryId | null;
|
||||
commands: {
|
||||
selectEntry: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
selectGroup: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
selectEntry: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
selectGroup: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
selectEdge: (direction: 'top' | 'bottom') => EntryId | null;
|
||||
selectPage: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
cloneEntry: (cursor: EntryId | null) => void;
|
||||
moveEntry: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
deleteAtCursor: (cursor: EntryId | null) => void;
|
||||
insertAtId: (patch: Partial<OntimeEntry> & { type: SupportedEntry }, id: EntryId | null, above?: boolean) => void;
|
||||
insertCopyAtId: (atId: EntryId | null, above?: boolean) => void;
|
||||
};
|
||||
clearSelectedEvents: () => void;
|
||||
setEntryCopyId: (id: EntryId | null) => void;
|
||||
setEntryCopyId: (id: EntryId | null, mode?: 'copy' | 'cut') => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a keyboard event target is a text input element.
|
||||
* Use this to avoid intercepting browser-native copy/cut/paste shortcuts
|
||||
* while users are typing in form fields.
|
||||
*/
|
||||
function isEditableElement(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tagName = target.tagName.toLowerCase();
|
||||
return tagName === 'input' || tagName === 'textarea';
|
||||
}
|
||||
|
||||
export function useRundownKeyboard({
|
||||
@@ -21,18 +41,89 @@ export function useRundownKeyboard({
|
||||
clearSelectedEvents,
|
||||
setEntryCopyId,
|
||||
}: UseRundownKeyboardOptions) {
|
||||
const scrollToEntry = useEventSelection((state) => state.scrollToEntry);
|
||||
|
||||
useHotkeys([
|
||||
['alt + ArrowDown', () => commands.selectEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + ArrowUp', () => commands.selectEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'alt + ArrowDown',
|
||||
() => {
|
||||
const nextId = commands.selectEntry(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'alt + ArrowUp',
|
||||
() => {
|
||||
const nextId = commands.selectEntry(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
[
|
||||
'alt + shift + ArrowDown',
|
||||
() => commands.selectGroup(cursor, 'down'),
|
||||
() => {
|
||||
const nextId = commands.selectGroup(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'alt + shift + ArrowUp',
|
||||
() => commands.selectGroup(cursor, 'up'),
|
||||
() => {
|
||||
const nextId = commands.selectGroup(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
[
|
||||
'Home',
|
||||
() => {
|
||||
const nextId = commands.selectEdge('top');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'End',
|
||||
() => {
|
||||
const nextId = commands.selectEdge('bottom');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'PageUp',
|
||||
() => {
|
||||
const nextId = commands.selectPage(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'PageDown',
|
||||
() => {
|
||||
const nextId = commands.selectPage(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
@@ -43,9 +134,16 @@ export function useRundownKeyboard({
|
||||
],
|
||||
['alt + mod + ArrowUp', () => commands.moveEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['Escape', () => clearSelectedEvents(), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'Escape',
|
||||
() => {
|
||||
clearSelectedEvents();
|
||||
setEntryCopyId(null);
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
['mod + Backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + Backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
[
|
||||
'alt + E',
|
||||
@@ -91,10 +189,50 @@ export function useRundownKeyboard({
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
['mod + C', () => setEntryCopyId(cursor)],
|
||||
['mod + V', () => commands.insertCopyAtId(cursor)],
|
||||
['mod + shift + V', () => commands.insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['alt + backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'mod + C',
|
||||
(event) => {
|
||||
if (cursor === null || isEditableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setEntryCopyId(cursor);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'mod + X',
|
||||
(event) => {
|
||||
if (cursor === null || isEditableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setEntryCopyId(cursor, 'cut');
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'mod + V',
|
||||
(event) => {
|
||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
commands.insertCopyAtId(cursor);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
['mod + D', () => commands.cloneEntry(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'mod + shift + V',
|
||||
(event) => {
|
||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
commands.insertCopyAtId(cursor, true);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ function FinderPlacement() {
|
||||
const [isOpen, handler] = useDisclosure();
|
||||
|
||||
useHotkeys([
|
||||
['mod + f', handler.toggle],
|
||||
['Escape', handler.close],
|
||||
['mod + f', handler.toggle, { preventDefault: true }],
|
||||
['Escape', handler.close, { preventDefault: true }],
|
||||
]);
|
||||
|
||||
if (isOpen) {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
&.copyTarget {
|
||||
outline: 1px dashed $blue-500;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.drag {
|
||||
|
||||
@@ -5,9 +5,11 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { OntimeDelay } from 'ontime-types';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import DelayInput from './DelayInput';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import DelayInput from './DelayInput';
|
||||
|
||||
import style from './RundownDelay.module.scss';
|
||||
|
||||
@@ -21,6 +23,7 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
|
||||
|
||||
const { applyDelay, deleteEntry } = useEntryActionsContext();
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
||||
|
||||
const {
|
||||
attributes: dragAttributes,
|
||||
@@ -59,7 +62,7 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.delay, hasCursor ? style.hasCursor : null])}
|
||||
className={cx([style.delay, hasCursor && style.hasCursor, entryCopyId === data.id && style.copyTarget])}
|
||||
ref={setNodeRef}
|
||||
style={dragStyle}
|
||||
data-testid='rundown-delay'
|
||||
|
||||
@@ -58,6 +58,10 @@ $skip-opacity: 0.2;
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
&.copyTarget {
|
||||
outline: 2px dashed $block-cursor-color;
|
||||
}
|
||||
|
||||
&.past:not(.skip) {
|
||||
.timerNote,
|
||||
.statusElements,
|
||||
|
||||
@@ -17,6 +17,8 @@ import { isPlaybackActive } from 'ontime-utils';
|
||||
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { deviceMod } from '../../../common/utils/deviceUtils';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useEventIdSwapping } from '../useEventIdSwapping';
|
||||
import { getSelectionMode, useEventSelection } from '../useEventSelection';
|
||||
@@ -105,6 +107,7 @@ export default function RundownEvent({
|
||||
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
@@ -143,6 +146,7 @@ export default function RundownEvent({
|
||||
type: 'item',
|
||||
label: 'Delete',
|
||||
icon: IoTrash,
|
||||
shortcut: `${deviceMod}+Del`,
|
||||
onClick: () => {
|
||||
clearSelectedEvents();
|
||||
deleteEntry(Array.from(selectedEvents));
|
||||
@@ -180,6 +184,7 @@ export default function RundownEvent({
|
||||
type: 'item',
|
||||
label: 'Clone',
|
||||
icon: IoDuplicateOutline,
|
||||
shortcut: `${deviceMod}+D`,
|
||||
onClick: () => clone(eventId, { after: eventId }),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
@@ -187,6 +192,7 @@ export default function RundownEvent({
|
||||
type: 'item',
|
||||
label: 'Delete',
|
||||
icon: IoTrash,
|
||||
shortcut: `${deviceMod}+Del`,
|
||||
onClick: () => {
|
||||
deleteEntry([eventId]);
|
||||
unselect(eventId);
|
||||
@@ -237,12 +243,13 @@ export default function RundownEvent({
|
||||
|
||||
const blockClasses = cx([
|
||||
style.rundownEvent,
|
||||
skip ? style.skip : null,
|
||||
isPast ? style.past : null,
|
||||
loaded ? style.loaded : null,
|
||||
playback ? style[playback] : null,
|
||||
isSelected ? style.selected : null,
|
||||
hasCursor ? style.hasCursor : null,
|
||||
skip && style.skip,
|
||||
isPast && style.past,
|
||||
loaded && style.loaded,
|
||||
playback && style[playback],
|
||||
isSelected && style.selected,
|
||||
hasCursor && style.hasCursor,
|
||||
entryCopyId === eventId && style.copyTarget,
|
||||
]);
|
||||
|
||||
const handleFocusClick = (event: MouseEvent) => {
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
&.copyTarget {
|
||||
outline: 2px dashed $block-cursor-color;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
margin-block: 0.5rem 0;
|
||||
border-radius: $block-border-radius $block-border-radius 0 0;
|
||||
|
||||
@@ -13,12 +13,14 @@ import { EntryId, OntimeGroup } from 'ontime-types';
|
||||
import { MILLIS_PER_MINUTE, millisToString } from 'ontime-utils';
|
||||
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { deviceMod } from '../../../common/utils/deviceUtils';
|
||||
import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { cx, getAccessibleColour, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration } from '../../../common/utils/time';
|
||||
import TitleEditor from '../common/TitleEditor';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { canDrop } from '../rundown.utils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
@@ -40,12 +42,14 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
||||
|
||||
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Clone Group',
|
||||
icon: IoDuplicateOutline,
|
||||
shortcut: `${deviceMod}+D`,
|
||||
onClick: () => clone(data.id),
|
||||
},
|
||||
{
|
||||
@@ -60,6 +64,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
||||
type: 'item',
|
||||
label: 'Delete Group',
|
||||
icon: IoTrash,
|
||||
shortcut: `${deviceMod}+Del`,
|
||||
onClick: () => deleteEntry([data.id]),
|
||||
},
|
||||
]);
|
||||
@@ -123,7 +128,12 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.group, hasCursor && style.hasCursor, !collapsed && style.expanded])}
|
||||
className={cx([
|
||||
style.group,
|
||||
hasCursor && style.hasCursor,
|
||||
!collapsed && style.expanded,
|
||||
entryCopyId === data.id && style.copyTarget,
|
||||
])}
|
||||
ref={setNodeRef}
|
||||
onClick={handleFocusClick}
|
||||
onContextMenu={onContextMenu}
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
&.copyTarget {
|
||||
outline: 2px dashed $block-cursor-color;
|
||||
}
|
||||
}
|
||||
|
||||
.binder {
|
||||
|
||||
@@ -6,9 +6,11 @@ import { EntryId } from 'ontime-types';
|
||||
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { deviceMod } from '../../../common/utils/deviceUtils';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RundownMilestone.module.scss';
|
||||
@@ -29,12 +31,14 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
||||
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
|
||||
const entryCopyId = useEntryCopy((state) => state.entryCopyId);
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Delete',
|
||||
icon: IoTrash,
|
||||
shortcut: `${deviceMod}+Del`,
|
||||
onClick: () => deleteEntry([entryId]),
|
||||
},
|
||||
]);
|
||||
@@ -82,7 +86,11 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.milestone, hasCursor ? style.hasCursor : null])}
|
||||
className={cx([
|
||||
style.milestone,
|
||||
hasCursor ? style.hasCursor : null,
|
||||
entryCopyId === entryId ? style.copyTarget : null,
|
||||
])}
|
||||
ref={setNodeRef}
|
||||
onClick={handleFocusClick}
|
||||
onContextMenu={onContextMenu}
|
||||
|
||||
@@ -152,7 +152,7 @@ export function moveUp(
|
||||
}
|
||||
|
||||
// 4. moving into the same group as previous entry
|
||||
if (isOntimeEvent(previousEntry) && previousEntry.parent !== null && currentEntryParent === null) {
|
||||
if ('parent' in previousEntry && previousEntry.parent !== null && currentEntryParent === null) {
|
||||
return { destinationId: previousEntryId, order: 'after' };
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export function moveDown(
|
||||
}
|
||||
|
||||
// 5. handle moving between group and top level
|
||||
const nextEntryParent = isOntimeEvent(nextEntry) ? nextEntry.parent : null;
|
||||
const nextEntryParent = 'parent' in nextEntry ? nextEntry.parent : null;
|
||||
if (nextEntryParent !== null && currentEntryParent === null) {
|
||||
return { destinationId: nextEntryId, order: 'after' };
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ interface EventSelectionStore {
|
||||
anchoredIndex: MaybeNumber;
|
||||
cursor: EntryId | null;
|
||||
entryMode: 'event' | 'single' | null;
|
||||
scrollHandler: ((id: EntryId) => void) | null;
|
||||
scrollHandlerSource: string | null;
|
||||
setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void;
|
||||
setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
|
||||
clearSelectedEvents: () => void;
|
||||
clearMultiSelect: () => void;
|
||||
unselect: (id: EntryId) => void;
|
||||
setScrollHandler: (source: string, handler: ((id: EntryId) => void) | null) => void;
|
||||
scrollToEntry: (id: EntryId) => void;
|
||||
}
|
||||
|
||||
export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
@@ -25,6 +29,8 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
anchoredIndex: null,
|
||||
cursor: null,
|
||||
entryMode: null,
|
||||
scrollHandler: null,
|
||||
scrollHandlerSource: null,
|
||||
setSingleEntrySelection: ({ id }) => {
|
||||
set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' });
|
||||
},
|
||||
@@ -99,8 +105,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
});
|
||||
}
|
||||
},
|
||||
clearSelectedEvents: () =>
|
||||
set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }),
|
||||
clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }),
|
||||
clearMultiSelect: () => {
|
||||
const { selectedEvents } = get();
|
||||
const [firstSelected] = selectedEvents;
|
||||
@@ -118,6 +123,22 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
entryMode: selectedEvents.size === 0 ? null : entryMode,
|
||||
});
|
||||
},
|
||||
setScrollHandler: (source, handler) =>
|
||||
set((state) => {
|
||||
if (handler) {
|
||||
return { scrollHandler: handler, scrollHandlerSource: source };
|
||||
}
|
||||
if (state.scrollHandlerSource !== source) {
|
||||
return state;
|
||||
}
|
||||
return { scrollHandler: null, scrollHandlerSource: null };
|
||||
}),
|
||||
scrollToEntry: (id: EntryId) => {
|
||||
const handler = get().scrollHandler;
|
||||
if (handler) {
|
||||
handler(id);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export function getSelectionMode(event: MouseEvent): SelectionMode {
|
||||
|
||||
@@ -45,6 +45,8 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
|
||||
|
||||
const selectedEventId = useSelectedEventId();
|
||||
const cursor = useEventSelection((state) => state.cursor);
|
||||
const scrollToEntry = useEventSelection((state) => state.scrollToEntry);
|
||||
const setScrollHandler = useEventSelection((state) => state.setScrollHandler);
|
||||
|
||||
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
|
||||
const { listeners } = useTableNav();
|
||||
@@ -114,24 +116,34 @@ export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cues
|
||||
setColumnSizing({});
|
||||
}, [setColumnSizing]);
|
||||
|
||||
// Follow selection changes depending on mode
|
||||
// Auto-scroll only in run mode, routed through the shared scroll handler
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current === null) {
|
||||
if (cuesheetMode !== AppMode.Run || !selectedEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = cuesheetMode === AppMode.Run ? selectedEventId : cursor ?? selectedEventId;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
scrollToEntry(selectedEventId);
|
||||
}, [cuesheetMode, data, selectedEventId, scrollToEntry]);
|
||||
|
||||
const eventIndex = data.findIndex((event) => event.id === targetId);
|
||||
if (eventIndex === -1) {
|
||||
return;
|
||||
}
|
||||
// Provide an imperative scroll handler for explicit jumps (finder/keyboard)
|
||||
useEffect(() => {
|
||||
setScrollHandler(`cuesheet-table-${tableRoot}`, (entryId) => {
|
||||
if (virtuosoRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'smooth', align: 'start', offset: -50 });
|
||||
}, [cuesheetMode, data, selectedEventId, cursor]);
|
||||
const eventIndex = data.findIndex((event) => event.id === entryId);
|
||||
if (eventIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtuosoRef.current.scrollToIndex({ index: eventIndex, behavior: 'smooth', align: 'start', offset: -50 });
|
||||
});
|
||||
|
||||
return () => {
|
||||
setScrollHandler(`cuesheet-table-${tableRoot}`, null);
|
||||
};
|
||||
}, [data, setScrollHandler, tableRoot]);
|
||||
|
||||
/**
|
||||
* To improve performance on resizing, we memoise the column sizes
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function useFinder() {
|
||||
const lastSearchString = useRef('');
|
||||
|
||||
const setSelectedEvents = useEventSelection((state) => state.setSelectedEvents);
|
||||
const scrollToEntry = useEventSelection((state) => state.scrollToEntry);
|
||||
|
||||
const [collapsedGroups, setCollapsedGroups] = useSessionStorage<EntryId[]>({
|
||||
// we ensure that this is unique to the rundown
|
||||
@@ -234,8 +235,9 @@ export default function useFinder() {
|
||||
|
||||
// Then select the event
|
||||
setSelectedEvents({ id: selectedEvent.id, index: selectedEvent.index, selectMode: 'click' });
|
||||
scrollToEntry(selectedEvent.id);
|
||||
},
|
||||
[collapsedGroups, setCollapsedGroups, setSelectedEvents],
|
||||
[collapsedGroups, setCollapsedGroups, setSelectedEvents, scrollToEntry],
|
||||
);
|
||||
|
||||
/** clear results when source data changes */
|
||||
|
||||
Reference in New Issue
Block a user