feat(timeline): add pulse animation for in-flight drag mutations

Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:28:46 +02:00
parent 7a5e98e2e9
commit 1df208dbcc
386 changed files with 657 additions and 81650 deletions
+15 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
import { useCallback, useDeferredValue, useEffect, useRef, useState, type MutableRefObject } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidateTimeline } from "./useInvalidatePlanningViews.js";
import { pixelsToDays } from "~/components/timeline/dragMath.js";
@@ -233,6 +233,7 @@ export function useTimelineDrag({
onAllocationMoved,
onShiftClickAlloc,
onMultiDragComplete,
onMutationError,
}: {
cellWidthRef: MutableRefObject<number>;
onShiftApplied?: (projectId: string) => void;
@@ -241,6 +242,7 @@ export function useTimelineDrag({
onAllocationMoved?: (snapshot: AllocationMovedSnapshot) => void;
onShiftClickAlloc?: (allocationId: string) => void;
onMultiDragComplete?: (daysDelta: number, mode: AllocDragMode, selectedIds?: string[]) => void;
onMutationError?: (message: string) => void;
}) {
const [dragState, setDragState] = useState<DragState>(INITIAL_DRAG_STATE);
const [allocDragState, setAllocDragState] = useState<AllocDragState>(INITIAL_ALLOC_DRAG);
@@ -278,6 +280,9 @@ export function useTimelineDrag({
const onMultiDragCompleteRef = useRef(onMultiDragComplete);
onMultiDragCompleteRef.current = onMultiDragComplete;
const onMutationErrorRef = useRef(onMutationError);
onMutationErrorRef.current = onMutationError;
const utils = trpc.useUtils();
const invalidateTimeline = useInvalidateTimeline();
@@ -387,6 +392,9 @@ export function useTimelineDrag({
setMultiSelectState(nextMultiSelectState);
}, []);
// Defer daysDelta to avoid firing the preview query every pixel during drag
const deferredDaysDelta = useDeferredValue(dragState.daysDelta);
// Project-shift preview
const { data: previewData, isFetching: isPreviewLoading } = trpc.timeline.previewShift.useQuery(
{
@@ -398,7 +406,7 @@ export function useTimelineDrag({
enabled:
dragState.isDragging &&
dragState.projectId !== null &&
dragState.daysDelta !== 0 &&
deferredDaysDelta !== 0 &&
dragState.currentStartDate !== null,
staleTime: 0,
},
@@ -447,7 +455,10 @@ export function useTimelineDrag({
pendingSnapshotRef.current = null;
}
},
onError: () => {
onError: (error) => {
console.error("[timeline] updateAllocationInline failed:", error);
const message = (error as { message?: string }).message ?? "Zuweisung konnte nicht verschoben werden.";
onMutationErrorRef.current?.(message);
clearPendingOptimisticAllocation();
},
});
@@ -653,6 +664,7 @@ export function useTimelineDrag({
extractAllocationFragment: extractAllocFragmentMutation.mutateAsync,
updateAllocation: updateAllocMutation.mutate,
clearPendingOptimisticAllocation,
onError: (msg) => onMutationErrorRef.current?.(msg),
});
allocDragRef.current = INITIAL_ALLOC_DRAG;