refactor: entry actions in cuesheet

fix: insert entry before
fix: avoid double submit on enter
fix: move events in the rundown
fix: clone groups
This commit is contained in:
Carlos Valente
2025-06-30 07:16:08 +02:00
committed by Carlos Valente
parent 8ad260d28a
commit 24a3823d3b
17 changed files with 592 additions and 301 deletions
+69 -59
View File
@@ -74,7 +74,7 @@ export const useEntryActions = () => {
* Calls mutation to add new entry
* @private
*/
const _addEntryMutation = useMutation({
const { mutateAsync: addEntryMutation } = useMutation({
// TODO(v4): optimistic create entry
mutationFn: postAddEntry,
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
@@ -143,13 +143,13 @@ export const useEntryActions = () => {
}
try {
await _addEntryMutation.mutateAsync(newEntry);
await addEntryMutation(newEntry);
} catch (error) {
logAxiosError('Failed adding event', error);
}
},
[
_addEntryMutation,
addEntryMutation,
defaultDangerTime,
defaultDuration,
defaultEndAction,
@@ -165,7 +165,7 @@ export const useEntryActions = () => {
* Calls mutation to clone a selection
* @private
*/
const _cloneMutation = useMutation({
const { mutateAsync: cloneEntryMutation } = useMutation({
mutationFn: postCloneEntry,
onSettled: () => queryClient.invalidateQueries({ queryKey: RUNDOWN }),
});
@@ -176,19 +176,19 @@ export const useEntryActions = () => {
const clone = useCallback(
async (entryId: EntryId) => {
try {
await _cloneMutation.mutateAsync(entryId);
await cloneEntryMutation(entryId);
} catch (error) {
logAxiosError('Error cloning entry', error);
}
},
[_cloneMutation],
[cloneEntryMutation],
);
/**
* Calls mutation to update existing entry
* @private
*/
const _updateEntryMutation = useMutation({
const { mutateAsync: updateEntryMutation } = useMutation({
mutationFn: putEditEntry,
// we optimistically update here
onMutate: async (newEvent) => {
@@ -234,12 +234,12 @@ export const useEntryActions = () => {
const updateEntry = useCallback(
async (event: Partial<OntimeEntry>) => {
try {
await _updateEntryMutation.mutateAsync(event);
await updateEntryMutation(event);
} catch (error) {
logAxiosError('Error updating event', error);
}
},
[_updateEntryMutation],
[updateEntryMutation],
);
const updateCustomField = useCallback(
@@ -287,7 +287,7 @@ export const useEntryActions = () => {
}
try {
await _updateEntryMutation.mutateAsync(newEvent);
await updateEntryMutation(newEvent);
} catch (error) {
logAxiosError('Error updating event', error);
}
@@ -339,14 +339,14 @@ export const useEntryActions = () => {
return previousEnd;
}
},
[_updateEntryMutation, queryClient],
[updateEntryMutation, queryClient],
);
/**
* Calls mutation to edit multiple events
* @private
*/
const _batchUpdateEventsMutation = useMutation({
const { mutateAsync: batchUpdateEventsMutation } = useMutation({
mutationFn: putBatchEditEvents,
onMutate: async ({ ids, data }) => {
// cancel ongoing queries
@@ -405,19 +405,19 @@ export const useEntryActions = () => {
const batchUpdateEvents = useCallback(
async (data: Partial<OntimeEvent>, eventIds: string[]) => {
try {
await _batchUpdateEventsMutation.mutateAsync({ ids: eventIds, data });
await batchUpdateEventsMutation({ ids: eventIds, data });
} catch (error) {
logAxiosError('Error updating events', error);
}
},
[_batchUpdateEventsMutation],
[batchUpdateEventsMutation],
);
/**
* Calls mutation to delete an entry
* @private
*/
const _deleteEntryMutation = useMutation({
const { mutateAsync: deleteEntryMutation } = useMutation({
mutationFn: deleteEntries,
// we optimistically update here
onMutate: async (entryIds: EntryId[]) => {
@@ -462,19 +462,19 @@ export const useEntryActions = () => {
const deleteEntry = useCallback(
async (entryIds: EntryId[]) => {
try {
await _deleteEntryMutation.mutateAsync(entryIds);
await deleteEntryMutation(entryIds);
} catch (error) {
logAxiosError('Error deleting event', error);
}
},
[_deleteEntryMutation],
[deleteEntryMutation],
);
/**
* Calls mutation to delete all events
* @private
*/
const _deleteAllEntriesMutation = useMutation({
const { mutateAsync: deleteAllEntriesMutation } = useMutation({
mutationFn: requestDeleteAll,
// we optimistically update here
onMutate: async () => {
@@ -514,17 +514,17 @@ export const useEntryActions = () => {
*/
const deleteAllEntries = useCallback(async () => {
try {
await _deleteAllEntriesMutation.mutateAsync();
await deleteAllEntriesMutation();
} catch (error) {
logAxiosError('Error deleting events', error);
}
}, [_deleteAllEntriesMutation]);
}, [deleteAllEntriesMutation]);
/**
* Calls mutation to apply a delay
* @private
*/
const _applyDelayMutation = useMutation({
const { mutateAsync: applyDelayMutation } = useMutation({
mutationFn: requestApplyDelay,
onSuccess: (response) => {
if (!response.data) return;
@@ -551,19 +551,19 @@ export const useEntryActions = () => {
const applyDelay = useCallback(
async (delayEventId: EntryId) => {
try {
await _applyDelayMutation.mutateAsync(delayEventId);
await applyDelayMutation(delayEventId);
} catch (error) {
logAxiosError('Error applying delay', error);
}
},
[_applyDelayMutation],
[applyDelayMutation],
);
/**
* Calls mutation to dissolve a block
* @private
*/
const _ungroupMutation = useMutation({
const { mutateAsync: ungroupMutation } = useMutation({
mutationFn: requestUngroup,
onSuccess: (response) => {
if (!response.data) return;
@@ -587,19 +587,19 @@ export const useEntryActions = () => {
const ungroup = useCallback(
async (blockId: EntryId) => {
try {
await _ungroupMutation.mutateAsync(blockId);
await ungroupMutation(blockId);
} catch (error) {
logAxiosError('Error dissolving block', error);
}
},
[_ungroupMutation],
[ungroupMutation],
);
/**
* Calls mutation to create a block with a selection
* @private
*/
const _groupEntriesMutation = useMutation({
const { mutateAsync: groupEntriesMutation } = useMutation({
mutationFn: requestGroupEntries,
onSuccess: (response) => {
if (!response.data) return;
@@ -623,19 +623,19 @@ export const useEntryActions = () => {
const groupEntries = useCallback(
async (entryIds: EntryId[]) => {
try {
await _groupEntriesMutation.mutateAsync(entryIds);
await groupEntriesMutation(entryIds);
} catch (error) {
logAxiosError('Error grouping entries', error);
}
},
[_groupEntriesMutation],
[groupEntriesMutation],
);
/**
* Calls mutation to reorder an entry
* @private
*/
const _reorderEntryMutation = useMutation({
const { mutateAsync: reorderEntryMutation } = useMutation({
mutationFn: patchReorderEntry,
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
@@ -644,6 +644,40 @@ export const useEntryActions = () => {
},
});
/**
* Reorders a given entry one step up or down in the timeline
*/
const move = useCallback(
async (entryId: EntryId, direction: 'up' | 'down') => {
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
if (!rundown) {
return;
}
const { destinationId, order } =
direction === 'up'
? moveUp(entryId, rundown.flatOrder, rundown.entries)
: moveDown(entryId, rundown.flatOrder, rundown.entries);
if (!destinationId) {
return; // noop
}
try {
const reorderObject: ReorderEntry = {
entryId,
destinationId,
order,
};
await reorderEntryMutation(reorderObject);
} catch (error) {
logAxiosError('Error re-ordering event', error);
}
// the rundown needs to know whether we moved into a block
return rundown.entries[destinationId]?.type === 'block' ? destinationId : undefined;
},
[queryClient, reorderEntryMutation],
);
/**
* Reorders a given entry
*/
@@ -655,43 +689,19 @@ export const useEntryActions = () => {
destinationId,
order,
};
await _reorderEntryMutation.mutateAsync(reorderObject);
await reorderEntryMutation(reorderObject);
} catch (error) {
logAxiosError('Error re-ordering event', error);
}
},
[_reorderEntryMutation],
[reorderEntryMutation],
);
const move = useCallback(async (entryId: EntryId, direction: 'up' | 'down') => {
const cachedRundown = queryClient.getQueryData<Rundown>(RUNDOWN);
if (!cachedRundown?.order) {
return;
}
const { destinationId, order } =
direction === 'up'
? moveUp(entryId, cachedRundown.order, cachedRundown.entries)
: moveDown(entryId, cachedRundown.order, cachedRundown.entries);
if (destinationId) {
try {
const reorderObject: ReorderEntry = {
entryId,
destinationId,
order: order as 'before' | 'after' | 'insert',
};
await _reorderEntryMutation.mutateAsync(reorderObject);
} catch (error) {
logAxiosError('Error re-ordering event', error);
}
}
}, []);
/**
* Calls mutation to swap events
* @private
*/
const _swapEvents = useMutation({
const { mutateAsync: swapEventsMutation } = useMutation({
mutationFn: requestEventSwap,
// we optimistically update here
onMutate: async ({ from, to }) => {
@@ -745,12 +755,12 @@ export const useEntryActions = () => {
const swapEvents = useCallback(
async ({ from, to }: SwapEntry) => {
try {
await _swapEvents.mutateAsync({ from, to });
await swapEventsMutation({ from, to });
} catch (error) {
logAxiosError('Error re-ordering event', error);
}
},
[_swapEvents],
[swapEventsMutation],
);
return {
@@ -1,4 +1,6 @@
import { MutableRefObject, useCallback, useEffect } from 'react';
import { MutableRefObject, useCallback, useEffect, useRef } from 'react';
import { useSelectedEventId } from './useSocket';
function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: MutableRefObject<ComponentRef>,
@@ -16,6 +18,23 @@ function scrollToComponent<ComponentRef extends HTMLElement, ScrollRef extends H
scrollRef.current.scrollTo({ top, behavior: 'smooth' });
}
function snapToComponent<ComponentRef extends HTMLElement, ScrollRef extends HTMLElement>(
componentRef: MutableRefObject<ComponentRef>,
scrollRef: MutableRefObject<ScrollRef>,
topOffset: number,
) {
if (!componentRef.current || !scrollRef.current) {
return;
}
const componentRect = componentRef.current.getBoundingClientRect();
const scrollRect = scrollRef.current.getBoundingClientRect();
const top = componentRect.top - scrollRect.top + scrollRef.current.scrollTop - topOffset;
// maintain current x scroll position
scrollRef.current.scrollTo(scrollRef.current.scrollLeft, top);
}
interface UseFollowComponentProps {
followRef: MutableRefObject<HTMLElement | null>;
scrollRef: MutableRefObject<HTMLElement | null>;
@@ -62,3 +81,32 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
return scrollToRefComponent;
}
export function useFollowSelected(doFollow: boolean, topOffset = 100) {
const selectedEvenId = useSelectedEventId();
const selectedRef = useRef<HTMLTableRowElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!doFollow) {
return;
}
if (selectedEvenId && selectedRef.current && scrollRef.current) {
// Use requestAnimationFrame to ensure the component is fully loaded
window.requestAnimationFrame(() => {
snapToComponent(
{ current: selectedRef.current } as MutableRefObject<HTMLElement>,
{ current: scrollRef.current } as MutableRefObject<HTMLElement>,
topOffset,
);
});
}
}, [doFollow, selectedEvenId, topOffset]);
return {
selectedRef,
scrollRef,
};
}