fix: keyboard shortcuts in rundown

This commit is contained in:
Carlos Valente
2025-07-14 20:58:50 +02:00
committed by Carlos Valente
parent ad2ef3b53f
commit abe9012f20
4 changed files with 69 additions and 45 deletions
+3 -11
View File
@@ -89,25 +89,17 @@ export const useEntryActions = () => {
// ************* CHECK OPTIONS specific to events // ************* CHECK OPTIONS specific to events
if (isOntimeEvent(newEntry)) { if (isOntimeEvent(newEntry)) {
// merge creation time options with event settings if (options?.lastEventId) {
const applicationOptions = {
after: options?.after,
before: options?.before,
lastEventId: options?.lastEventId,
linkPrevious: options?.linkPrevious ?? linkPrevious,
};
if (applicationOptions?.lastEventId) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know this is a value
const rundownData = queryClient.getQueryData<Rundown>(RUNDOWN)!; const rundownData = queryClient.getQueryData<Rundown>(RUNDOWN)!;
const previousEvent = rundownData.entries[applicationOptions.lastEventId]; const previousEvent = rundownData.entries[options?.lastEventId];
if (isOntimeEvent(previousEvent)) { if (isOntimeEvent(previousEvent)) {
newEntry.timeStart = previousEvent.timeEnd; newEntry.timeStart = previousEvent.timeEnd;
} }
} }
// Override event with options from editor settings // Override event with options from editor settings
newEntry.linkStart = applicationOptions.linkPrevious; newEntry.linkStart = options?.linkPrevious ?? linkPrevious;
if (newEntry.duration === undefined && newEntry.timeEnd === undefined) { if (newEntry.duration === undefined && newEntry.timeEnd === undefined) {
newEntry.duration = parseUserTime(defaultDuration); newEntry.duration = parseUserTime(defaultDuration);
+43 -27
View File
@@ -33,7 +33,7 @@ import {
reorderArray, reorderArray,
} from 'ontime-utils'; } from 'ontime-utils';
import { type EventOptions, useEntryActions } from '../../common/hooks/useEntryAction'; import { useEntryActions } from '../../common/hooks/useEntryAction';
import useFollowComponent from '../../common/hooks/useFollowComponent'; import useFollowComponent from '../../common/hooks/useFollowComponent';
import { useRundownEditor } from '../../common/hooks/useSocket'; import { useRundownEditor } from '../../common/hooks/useSocket';
import { useEntryCopy } from '../../common/stores/entryCopyStore'; import { useEntryCopy } from '../../common/stores/entryCopyStore';
@@ -114,20 +114,16 @@ export default function Rundown({ data }: RundownProps) {
[addEntry, order, entries], [addEntry, order, entries],
); );
/**
* Add a new item referring to an existing one
*/
const insertAtId = useCallback( const insertAtId = useCallback(
(patch: Partial<OntimeEntry> & { type: SupportedEntry }, id: MaybeString, above = false) => { (patch: Partial<OntimeEntry> & { type: SupportedEntry }, id: MaybeString, above = false) => {
const options: EventOptions = addEntry(patch, {
id === null after: id && !above ? id : undefined,
? {} before: id && above ? id : undefined,
: { lastEventId: !above && id ? id : undefined,
after: above ? undefined : id, });
before: above ? id : undefined,
};
if (!above && id) {
options.lastEventId = id;
}
addEntry(patch, options);
}, },
[addEntry], [addEntry],
); );
@@ -239,52 +235,72 @@ export default function Rundown({ data }: RundownProps) {
// shortcuts // shortcuts
useHotkeys([ useHotkeys([
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true }], ['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true }], ['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
['alt + shift + ArrowDown', () => selectBlock(cursor, 'down'), { preventDefault: true }], ['alt + shift + ArrowDown', () => selectBlock(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
['alt + shift + ArrowUp', () => selectBlock(cursor, 'up'), { preventDefault: true }], ['alt + shift + ArrowUp', () => selectBlock(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true }], ['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true }], ['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
['Escape', () => clearSelectedEvents(), { preventDefault: true }], ['Escape', () => clearSelectedEvents(), { preventDefault: true, usePhysicalKeys: true }],
['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true }], ['mod + Backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
[ [
'alt + E', 'alt + E',
() => insertAtId({ type: SupportedEntry.Event }, cursor), () => insertAtId({ type: SupportedEntry.Event }, cursor),
{ preventDefault: true, usePhysicalKeys: true }, { preventDefault: true, usePhysicalKeys: true },
], ],
['alt + shift + E', () => insertAtId({ type: SupportedEntry.Event }, cursor, true), { preventDefault: true }], [
'alt + shift + E',
() => insertAtId({ type: SupportedEntry.Event }, cursor, true),
{ preventDefault: true, usePhysicalKeys: true },
],
[ [
'alt + G', 'alt + G',
() => insertAtId({ type: SupportedEntry.Block }, cursor), () => insertAtId({ type: SupportedEntry.Block }, cursor),
{ preventDefault: true, usePhysicalKeys: true }, { preventDefault: true, usePhysicalKeys: true },
], ],
['alt + shift + G', () => insertAtId({ type: SupportedEntry.Block }, cursor, true), { preventDefault: true }], [
'alt + shift + G',
() => insertAtId({ type: SupportedEntry.Block }, cursor, true),
{ preventDefault: true, usePhysicalKeys: true },
],
[ [
'alt + D', 'alt + D',
() => insertAtId({ type: SupportedEntry.Delay }, cursor), () => insertAtId({ type: SupportedEntry.Delay }, cursor),
{ preventDefault: true, usePhysicalKeys: true }, { preventDefault: true, usePhysicalKeys: true },
], ],
['alt + shift + D', () => insertAtId({ type: SupportedEntry.Delay }, cursor, true), { preventDefault: true }], [
'alt + shift + D',
() => insertAtId({ type: SupportedEntry.Delay }, cursor, true),
{ preventDefault: true, usePhysicalKeys: true },
],
[ [
'alt + M', 'alt + M',
() => insertAtId({ type: SupportedEntry.Milestone }, cursor), () => insertAtId({ type: SupportedEntry.Milestone }, cursor),
{ preventDefault: true, usePhysicalKeys: true }, { preventDefault: true, usePhysicalKeys: true },
], ],
['alt + shift + M', () => insertAtId({ type: SupportedEntry.Milestone }, cursor, true), { preventDefault: true }], [
'alt + shift + M',
() => insertAtId({ type: SupportedEntry.Milestone }, cursor, true),
{ preventDefault: true, usePhysicalKeys: true },
],
['mod + C', () => setEntryCopyId(cursor)], ['mod + C', () => setEntryCopyId(cursor)],
['mod + V', () => insertCopyAtId(cursor, entryCopyId)], ['mod + V', () => insertCopyAtId(cursor, entryCopyId)],
['mod + shift + V', () => insertCopyAtId(cursor, entryCopyId, true), { preventDefault: true }], [
'mod + shift + V',
() => insertCopyAtId(cursor, entryCopyId, true),
{ preventDefault: true, usePhysicalKeys: true },
],
['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true }], ['alt + backspace', () => deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
]); ]);
// we copy the state from the store here // we copy the state from the store here
@@ -35,14 +35,30 @@ export async function addEntry(eventData: EventPostPayload): Promise<OntimeEntry
throw new Error(`Event with ID ${eventData.id} already exists`); throw new Error(`Event with ID ${eventData.id} already exists`);
} }
// if the user provides a parent (inside a group), we make sure it exists and it is a group // the parent can be provided or inferred from position
let parent: OntimeBlock | null = null; let parent: OntimeBlock | null = null;
console.log('Adding entry with data:', eventData);
if ('parent' in eventData && eventData.parent != null) { if ('parent' in eventData && eventData.parent != null) {
// if the user provides a parent (inside a group), we make sure it exists and it is a group
const maybeParent = rundown.entries[eventData.parent]; const maybeParent = rundown.entries[eventData.parent];
if (!maybeParent || !isOntimeBlock(maybeParent)) { if (!maybeParent || !isOntimeBlock(maybeParent)) {
throw new Error(`Invalid parent event with ID ${eventData.parent}`); throw new Error(`Invalid parent event with ID ${eventData.parent}`);
} }
parent = maybeParent; parent = maybeParent;
} else {
// otherwise, we may infer the parent from relative positioning (after/before)
const referenceId = eventData?.after ?? eventData?.before;
if (referenceId) {
const maybeSibling = rundown.entries[referenceId];
if (maybeSibling && 'parent' in maybeSibling && maybeSibling.parent) {
const maybeParent = rundown.entries[maybeSibling.parent];
if (maybeParent && isOntimeBlock(maybeParent)) {
parent = maybeParent;
}
}
}
} }
// normalise the position of the event in the rundown order // normalise the position of the event in the rundown order
@@ -1,15 +1,15 @@
import type { OntimeEntry } from '../../definitions/core/OntimeEntry.js'; import type { EntryId, OntimeEntry } from '../../definitions/core/OntimeEntry.js';
export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: string }; export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId };
export type EventPostPayload = Partial<OntimeEntry> & { export type EventPostPayload = Partial<OntimeEntry> & {
after?: string; after?: EntryId;
before?: string; before?: EntryId;
}; };
export type TransientEventPayload = Partial<OntimeEntry> & { export type TransientEventPayload = Partial<OntimeEntry> & {
after?: string; after?: EntryId;
before?: string; before?: EntryId;
}; };
export type ProjectRundown = { export type ProjectRundown = {