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 {
|
||||
captureLivePreviewTargets,
|
||||
clearLivePreview,
|
||||
datesMatch,
|
||||
preserveLivePreview,
|
||||
scheduleLivePreview,
|
||||
type LivePreviewSession,
|
||||
} from "./timelineLivePreview.js";
|
||||
import {
|
||||
buildAllocationMovedSnapshot,
|
||||
hasAllocationDateChange,
|
||||
requiresAllocationFragmentExtraction,
|
||||
shouldTreatAllocationDragAsClick,
|
||||
} from "./timelineAllocationFinalize.js";
|
||||
import {
|
||||
createMultiSelectState,
|
||||
finalizeMultiSelectDraft,
|
||||
@@ -729,23 +734,14 @@ export function useTimelineDrag({
|
||||
updateAllocationDragPosition(ev.clientX);
|
||||
const alloc = allocDragRef.current;
|
||||
if (!alloc.isActive) return;
|
||||
const pointerDelta = Math.abs(alloc.pointerDeltaX);
|
||||
const hasDateChange =
|
||||
Boolean(alloc.originalStartDate && alloc.currentStartDate && alloc.originalEndDate && alloc.currentEndDate) &&
|
||||
(
|
||||
alloc.originalStartDate!.getTime() !== alloc.currentStartDate!.getTime() ||
|
||||
alloc.originalEndDate!.getTime() !== alloc.currentEndDate!.getTime()
|
||||
);
|
||||
const hasDateChange = hasAllocationDateChange(alloc);
|
||||
|
||||
if (hasDateChange) {
|
||||
preserveLivePreview(allocPreviewRef.current);
|
||||
}
|
||||
clearLivePreview(allocPreviewRef.current);
|
||||
allocPreviewRef.current = null;
|
||||
const shouldTreatAsClick =
|
||||
alloc.mode === "move" &&
|
||||
alloc.daysDelta === 0 &&
|
||||
pointerDelta <= DRAG_CLICK_THRESHOLD_PX;
|
||||
const shouldTreatAsClick = shouldTreatAllocationDragAsClick(alloc, DRAG_CLICK_THRESHOLD_PX);
|
||||
|
||||
if (shouldTreatAsClick && alloc.allocationId) {
|
||||
// No movement → treat as click
|
||||
@@ -767,18 +763,9 @@ export function useTimelineDrag({
|
||||
const currentStartDate = alloc.currentStartDate;
|
||||
const currentEndDate = alloc.currentEndDate;
|
||||
const baseMutationAllocationId = alloc.mutationAllocationId ?? activeAllocationId;
|
||||
const requiresExtraction =
|
||||
alloc.scope === "segment" &&
|
||||
(!datesMatch(alloc.originalStartDate, alloc.allocationStartDate) ||
|
||||
!datesMatch(alloc.originalEndDate, alloc.allocationEndDate));
|
||||
const requiresExtraction = requiresAllocationFragmentExtraction(alloc);
|
||||
|
||||
pendingSnapshotRef.current = {
|
||||
allocationId: activeAllocationId,
|
||||
mutationAllocationId: baseMutationAllocationId,
|
||||
projectName: alloc.projectName ?? "",
|
||||
before: { startDate: alloc.originalStartDate!, endDate: alloc.originalEndDate! },
|
||||
after: { startDate: currentStartDate, endDate: currentEndDate },
|
||||
};
|
||||
pendingSnapshotRef.current = buildAllocationMovedSnapshot(alloc);
|
||||
pendingOptimisticAllocationIdRef.current = activeAllocationId;
|
||||
setOptimisticAllocations((prev) => {
|
||||
const next = new Map(prev);
|
||||
|
||||
Reference in New Issue
Block a user