refactor(web): extract allocation drag finalize helpers
This commit is contained in:
@@ -0,0 +1,148 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildAllocationMovedSnapshot,
|
||||||
|
hasAllocationDateChange,
|
||||||
|
requiresAllocationFragmentExtraction,
|
||||||
|
shouldTreatAllocationDragAsClick,
|
||||||
|
} from "./timelineAllocationFinalize.js";
|
||||||
|
|
||||||
|
const baseDates = {
|
||||||
|
allocationStartDate: new Date("2025-01-01T00:00:00.000Z"),
|
||||||
|
allocationEndDate: new Date("2025-01-05T00:00:00.000Z"),
|
||||||
|
originalStartDate: new Date("2025-01-01T00:00:00.000Z"),
|
||||||
|
originalEndDate: new Date("2025-01-05T00:00:00.000Z"),
|
||||||
|
currentStartDate: new Date("2025-01-01T00:00:00.000Z"),
|
||||||
|
currentEndDate: new Date("2025-01-05T00:00:00.000Z"),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
describe("timelineAllocationFinalize", () => {
|
||||||
|
it("treats missing dates as no change", () => {
|
||||||
|
expect(
|
||||||
|
hasAllocationDateChange({
|
||||||
|
mode: "move",
|
||||||
|
scope: "allocation",
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: "Alpha",
|
||||||
|
allocationStartDate: null,
|
||||||
|
allocationEndDate: null,
|
||||||
|
originalStartDate: null,
|
||||||
|
originalEndDate: baseDates.originalEndDate,
|
||||||
|
currentStartDate: baseDates.currentStartDate,
|
||||||
|
currentEndDate: baseDates.currentEndDate,
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 0,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects start or end date changes", () => {
|
||||||
|
expect(
|
||||||
|
hasAllocationDateChange({
|
||||||
|
mode: "move",
|
||||||
|
scope: "allocation",
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: "Alpha",
|
||||||
|
...baseDates,
|
||||||
|
currentEndDate: new Date("2025-01-06T00:00:00.000Z"),
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 1,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats only small move drags without day changes as clicks", () => {
|
||||||
|
expect(
|
||||||
|
shouldTreatAllocationDragAsClick(
|
||||||
|
{ mode: "move", daysDelta: 0, pointerDeltaX: -5 },
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldTreatAllocationDragAsClick(
|
||||||
|
{ mode: "resize-end", daysDelta: 0, pointerDeltaX: 0 },
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldTreatAllocationDragAsClick(
|
||||||
|
{ mode: "move", daysDelta: 1, pointerDeltaX: 2 },
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires extraction only for segment drags that no longer match allocation boundaries", () => {
|
||||||
|
expect(
|
||||||
|
requiresAllocationFragmentExtraction({
|
||||||
|
mode: "move",
|
||||||
|
scope: "segment",
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: "Alpha",
|
||||||
|
...baseDates,
|
||||||
|
allocationStartDate: new Date("2024-12-31T00:00:00.000Z"),
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 1,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
requiresAllocationFragmentExtraction({
|
||||||
|
mode: "move",
|
||||||
|
scope: "allocation",
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: "Alpha",
|
||||||
|
...baseDates,
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 1,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null snapshots when required dates or ids are missing", () => {
|
||||||
|
expect(
|
||||||
|
buildAllocationMovedSnapshot({
|
||||||
|
mode: "move",
|
||||||
|
scope: "allocation",
|
||||||
|
allocationId: null,
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: "Alpha",
|
||||||
|
...baseDates,
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 1,
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a mutation snapshot with fallback mutation allocation ids", () => {
|
||||||
|
expect(
|
||||||
|
buildAllocationMovedSnapshot({
|
||||||
|
mode: "move",
|
||||||
|
scope: "allocation",
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: null,
|
||||||
|
projectName: null,
|
||||||
|
...baseDates,
|
||||||
|
currentStartDate: new Date("2025-01-02T00:00:00.000Z"),
|
||||||
|
currentEndDate: new Date("2025-01-06T00:00:00.000Z"),
|
||||||
|
pointerDeltaX: 32,
|
||||||
|
daysDelta: 1,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: "alloc-1",
|
||||||
|
projectName: "",
|
||||||
|
before: {
|
||||||
|
startDate: new Date("2025-01-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-05T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
startDate: new Date("2025-01-02T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-06T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { datesMatch } from "./timelineLivePreview.js";
|
||||||
|
|
||||||
|
export type AllocationMovedSnapshotLike = {
|
||||||
|
allocationId: string;
|
||||||
|
mutationAllocationId: string;
|
||||||
|
projectName: string;
|
||||||
|
before: { startDate: Date; endDate: Date };
|
||||||
|
after: { startDate: Date; endDate: Date };
|
||||||
|
};
|
||||||
|
|
||||||
|
type AllocDragFinalizeLike = {
|
||||||
|
mode: "move" | "resize-start" | "resize-end";
|
||||||
|
scope: "allocation" | "segment";
|
||||||
|
allocationId: string | null;
|
||||||
|
mutationAllocationId: string | null;
|
||||||
|
projectName: string | null;
|
||||||
|
allocationStartDate: Date | null;
|
||||||
|
allocationEndDate: Date | null;
|
||||||
|
originalStartDate: Date | null;
|
||||||
|
originalEndDate: Date | null;
|
||||||
|
currentStartDate: Date | null;
|
||||||
|
currentEndDate: Date | null;
|
||||||
|
pointerDeltaX: number;
|
||||||
|
daysDelta: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function hasAllocationDateChange(alloc: AllocDragFinalizeLike): boolean {
|
||||||
|
if (!alloc.originalStartDate || !alloc.currentStartDate || !alloc.originalEndDate || !alloc.currentEndDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
alloc.originalStartDate.getTime() !== alloc.currentStartDate.getTime() ||
|
||||||
|
alloc.originalEndDate.getTime() !== alloc.currentEndDate.getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldTreatAllocationDragAsClick(
|
||||||
|
alloc: Pick<AllocDragFinalizeLike, "mode" | "daysDelta" | "pointerDeltaX">,
|
||||||
|
clickThresholdPx: number,
|
||||||
|
): boolean {
|
||||||
|
return alloc.mode === "move" && alloc.daysDelta === 0 && Math.abs(alloc.pointerDeltaX) <= clickThresholdPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requiresAllocationFragmentExtraction(alloc: AllocDragFinalizeLike): boolean {
|
||||||
|
return (
|
||||||
|
alloc.scope === "segment" &&
|
||||||
|
(!datesMatch(alloc.originalStartDate, alloc.allocationStartDate) ||
|
||||||
|
!datesMatch(alloc.originalEndDate, alloc.allocationEndDate))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAllocationMovedSnapshot(
|
||||||
|
alloc: AllocDragFinalizeLike,
|
||||||
|
): AllocationMovedSnapshotLike | null {
|
||||||
|
if (
|
||||||
|
!alloc.allocationId ||
|
||||||
|
!alloc.originalStartDate ||
|
||||||
|
!alloc.originalEndDate ||
|
||||||
|
!alloc.currentStartDate ||
|
||||||
|
!alloc.currentEndDate
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allocationId: alloc.allocationId,
|
||||||
|
mutationAllocationId: alloc.mutationAllocationId ?? alloc.allocationId,
|
||||||
|
projectName: alloc.projectName ?? "",
|
||||||
|
before: {
|
||||||
|
startDate: alloc.originalStartDate,
|
||||||
|
endDate: alloc.originalEndDate,
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
startDate: alloc.currentStartDate,
|
||||||
|
endDate: alloc.currentEndDate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,11 +7,16 @@ import { pixelsToDays, computeDragDates } from "~/components/timeline/dragMath.j
|
|||||||
import {
|
import {
|
||||||
captureLivePreviewTargets,
|
captureLivePreviewTargets,
|
||||||
clearLivePreview,
|
clearLivePreview,
|
||||||
datesMatch,
|
|
||||||
preserveLivePreview,
|
preserveLivePreview,
|
||||||
scheduleLivePreview,
|
scheduleLivePreview,
|
||||||
type LivePreviewSession,
|
type LivePreviewSession,
|
||||||
} from "./timelineLivePreview.js";
|
} from "./timelineLivePreview.js";
|
||||||
|
import {
|
||||||
|
buildAllocationMovedSnapshot,
|
||||||
|
hasAllocationDateChange,
|
||||||
|
requiresAllocationFragmentExtraction,
|
||||||
|
shouldTreatAllocationDragAsClick,
|
||||||
|
} from "./timelineAllocationFinalize.js";
|
||||||
import {
|
import {
|
||||||
createMultiSelectState,
|
createMultiSelectState,
|
||||||
finalizeMultiSelectDraft,
|
finalizeMultiSelectDraft,
|
||||||
@@ -729,23 +734,14 @@ export function useTimelineDrag({
|
|||||||
updateAllocationDragPosition(ev.clientX);
|
updateAllocationDragPosition(ev.clientX);
|
||||||
const alloc = allocDragRef.current;
|
const alloc = allocDragRef.current;
|
||||||
if (!alloc.isActive) return;
|
if (!alloc.isActive) return;
|
||||||
const pointerDelta = Math.abs(alloc.pointerDeltaX);
|
const hasDateChange = hasAllocationDateChange(alloc);
|
||||||
const hasDateChange =
|
|
||||||
Boolean(alloc.originalStartDate && alloc.currentStartDate && alloc.originalEndDate && alloc.currentEndDate) &&
|
|
||||||
(
|
|
||||||
alloc.originalStartDate!.getTime() !== alloc.currentStartDate!.getTime() ||
|
|
||||||
alloc.originalEndDate!.getTime() !== alloc.currentEndDate!.getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasDateChange) {
|
if (hasDateChange) {
|
||||||
preserveLivePreview(allocPreviewRef.current);
|
preserveLivePreview(allocPreviewRef.current);
|
||||||
}
|
}
|
||||||
clearLivePreview(allocPreviewRef.current);
|
clearLivePreview(allocPreviewRef.current);
|
||||||
allocPreviewRef.current = null;
|
allocPreviewRef.current = null;
|
||||||
const shouldTreatAsClick =
|
const shouldTreatAsClick = shouldTreatAllocationDragAsClick(alloc, DRAG_CLICK_THRESHOLD_PX);
|
||||||
alloc.mode === "move" &&
|
|
||||||
alloc.daysDelta === 0 &&
|
|
||||||
pointerDelta <= DRAG_CLICK_THRESHOLD_PX;
|
|
||||||
|
|
||||||
if (shouldTreatAsClick && alloc.allocationId) {
|
if (shouldTreatAsClick && alloc.allocationId) {
|
||||||
// No movement → treat as click
|
// No movement → treat as click
|
||||||
@@ -767,18 +763,9 @@ export function useTimelineDrag({
|
|||||||
const currentStartDate = alloc.currentStartDate;
|
const currentStartDate = alloc.currentStartDate;
|
||||||
const currentEndDate = alloc.currentEndDate;
|
const currentEndDate = alloc.currentEndDate;
|
||||||
const baseMutationAllocationId = alloc.mutationAllocationId ?? activeAllocationId;
|
const baseMutationAllocationId = alloc.mutationAllocationId ?? activeAllocationId;
|
||||||
const requiresExtraction =
|
const requiresExtraction = requiresAllocationFragmentExtraction(alloc);
|
||||||
alloc.scope === "segment" &&
|
|
||||||
(!datesMatch(alloc.originalStartDate, alloc.allocationStartDate) ||
|
|
||||||
!datesMatch(alloc.originalEndDate, alloc.allocationEndDate));
|
|
||||||
|
|
||||||
pendingSnapshotRef.current = {
|
pendingSnapshotRef.current = buildAllocationMovedSnapshot(alloc);
|
||||||
allocationId: activeAllocationId,
|
|
||||||
mutationAllocationId: baseMutationAllocationId,
|
|
||||||
projectName: alloc.projectName ?? "",
|
|
||||||
before: { startDate: alloc.originalStartDate!, endDate: alloc.originalEndDate! },
|
|
||||||
after: { startDate: currentStartDate, endDate: currentEndDate },
|
|
||||||
};
|
|
||||||
pendingOptimisticAllocationIdRef.current = activeAllocationId;
|
pendingOptimisticAllocationIdRef.current = activeAllocationId;
|
||||||
setOptimisticAllocations((prev) => {
|
setOptimisticAllocations((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
|
|||||||
Reference in New Issue
Block a user