refactor(web): extract allocation release effects
This commit is contained in:
@@ -0,0 +1,270 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { finalizeAllocationReleaseEffects } from "./timelineAllocationReleaseEffects.js";
|
||||||
|
|
||||||
|
const BASE_ALLOC = {
|
||||||
|
isActive: true,
|
||||||
|
mode: "move" as const,
|
||||||
|
scope: "allocation" as const,
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: "alloc-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
projectName: "Project One",
|
||||||
|
allocationStartDate: new Date("2025-01-01"),
|
||||||
|
allocationEndDate: new Date("2025-01-03"),
|
||||||
|
originalStartDate: new Date("2025-01-01"),
|
||||||
|
originalEndDate: new Date("2025-01-03"),
|
||||||
|
currentStartDate: new Date("2025-01-01"),
|
||||||
|
currentEndDate: new Date("2025-01-03"),
|
||||||
|
pointerDeltaX: 0,
|
||||||
|
daysDelta: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function createOptimisticStateHarness() {
|
||||||
|
let optimistic = new Map<string, { startDate: Date; endDate: Date }>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return optimistic;
|
||||||
|
},
|
||||||
|
setOptimisticAllocations(
|
||||||
|
updater: (
|
||||||
|
prev: Map<string, { startDate: Date; endDate: Date }>,
|
||||||
|
) => Map<string, { startDate: Date; endDate: Date }>,
|
||||||
|
) {
|
||||||
|
optimistic = updater(optimistic);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("timelineAllocationReleaseEffects", () => {
|
||||||
|
it("ignores inactive releases without touching preview or optimistic state", async () => {
|
||||||
|
const updatePosition = vi.fn();
|
||||||
|
const preserve = vi.fn();
|
||||||
|
const clear = vi.fn();
|
||||||
|
const onShiftClick = vi.fn();
|
||||||
|
const onBlockClick = vi.fn();
|
||||||
|
const optimisticHarness = createOptimisticStateHarness();
|
||||||
|
const pendingSnapshotRef = { current: null };
|
||||||
|
const pendingOptimisticAllocationIdRef = { current: null as string | null };
|
||||||
|
|
||||||
|
await finalizeAllocationReleaseEffects({
|
||||||
|
clientX: 24,
|
||||||
|
allocRef: { current: { ...BASE_ALLOC, isActive: false } },
|
||||||
|
previewRef: { current: {} as never },
|
||||||
|
updatePosition,
|
||||||
|
clickThresholdPx: 5,
|
||||||
|
wasShift: false,
|
||||||
|
onShiftClick,
|
||||||
|
onBlockClick,
|
||||||
|
pendingSnapshotRef,
|
||||||
|
pendingOptimisticAllocationIdRef,
|
||||||
|
setOptimisticAllocations: optimisticHarness.setOptimisticAllocations,
|
||||||
|
extractAllocationFragment: vi.fn(),
|
||||||
|
updateAllocation: vi.fn(),
|
||||||
|
clearPendingOptimisticAllocation: vi.fn(),
|
||||||
|
previewOps: { preserve, clear },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatePosition).toHaveBeenCalledWith(24);
|
||||||
|
expect(preserve).not.toHaveBeenCalled();
|
||||||
|
expect(clear).not.toHaveBeenCalled();
|
||||||
|
expect(onShiftClick).not.toHaveBeenCalled();
|
||||||
|
expect(onBlockClick).not.toHaveBeenCalled();
|
||||||
|
expect(pendingSnapshotRef.current).toBeNull();
|
||||||
|
expect(pendingOptimisticAllocationIdRef.current).toBeNull();
|
||||||
|
expect(optimisticHarness.value.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears preview and dispatches click releases without starting mutations", async () => {
|
||||||
|
const clear = vi.fn();
|
||||||
|
const onBlockClick = vi.fn();
|
||||||
|
const optimisticHarness = createOptimisticStateHarness();
|
||||||
|
const previewRef = { current: {} as never };
|
||||||
|
|
||||||
|
await finalizeAllocationReleaseEffects({
|
||||||
|
clientX: 40,
|
||||||
|
allocRef: { current: BASE_ALLOC },
|
||||||
|
previewRef,
|
||||||
|
updatePosition: vi.fn(),
|
||||||
|
clickThresholdPx: 5,
|
||||||
|
wasShift: false,
|
||||||
|
onBlockClick,
|
||||||
|
pendingSnapshotRef: { current: null },
|
||||||
|
pendingOptimisticAllocationIdRef: { current: null },
|
||||||
|
setOptimisticAllocations: optimisticHarness.setOptimisticAllocations,
|
||||||
|
extractAllocationFragment: vi.fn(),
|
||||||
|
updateAllocation: vi.fn(),
|
||||||
|
clearPendingOptimisticAllocation: vi.fn(),
|
||||||
|
resolveRelease: () => ({
|
||||||
|
kind: "click",
|
||||||
|
clickInfo: {
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
projectName: "Project One",
|
||||||
|
startDate: new Date("2025-01-01"),
|
||||||
|
endDate: new Date("2025-01-03"),
|
||||||
|
},
|
||||||
|
preservePreview: true,
|
||||||
|
}),
|
||||||
|
previewOps: { preserve: vi.fn(), clear },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(clear).toHaveBeenCalledOnce();
|
||||||
|
expect(onBlockClick).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ allocationId: "alloc-1", projectId: "project-1" }),
|
||||||
|
);
|
||||||
|
expect(previewRef.current).toBeNull();
|
||||||
|
expect(optimisticHarness.value.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts optimistic mutation flow and rewrites the pending snapshot after extraction", async () => {
|
||||||
|
const preserve = vi.fn();
|
||||||
|
const clear = vi.fn();
|
||||||
|
const extractAllocationFragment = vi.fn().mockResolvedValue({
|
||||||
|
extractedAllocationId: "alloc-fragment-2",
|
||||||
|
});
|
||||||
|
const updateAllocation = vi.fn();
|
||||||
|
const optimisticHarness = createOptimisticStateHarness();
|
||||||
|
const pendingSnapshotRef = {
|
||||||
|
current: null as {
|
||||||
|
allocationId: string;
|
||||||
|
mutationAllocationId: string;
|
||||||
|
projectName: string;
|
||||||
|
before: { startDate: Date; endDate: Date };
|
||||||
|
after: { startDate: Date; endDate: Date };
|
||||||
|
} | null,
|
||||||
|
};
|
||||||
|
const pendingOptimisticAllocationIdRef = { current: null as string | null };
|
||||||
|
|
||||||
|
await finalizeAllocationReleaseEffects({
|
||||||
|
clientX: 44,
|
||||||
|
allocRef: { current: BASE_ALLOC },
|
||||||
|
previewRef: { current: {} as never },
|
||||||
|
updatePosition: vi.fn(),
|
||||||
|
clickThresholdPx: 5,
|
||||||
|
wasShift: false,
|
||||||
|
pendingSnapshotRef,
|
||||||
|
pendingOptimisticAllocationIdRef,
|
||||||
|
setOptimisticAllocations: optimisticHarness.setOptimisticAllocations,
|
||||||
|
extractAllocationFragment,
|
||||||
|
updateAllocation,
|
||||||
|
clearPendingOptimisticAllocation: vi.fn(),
|
||||||
|
resolveRelease: () => ({
|
||||||
|
kind: "mutation",
|
||||||
|
preservePreview: true,
|
||||||
|
mutationPlan: {
|
||||||
|
activeAllocationId: "alloc-1",
|
||||||
|
currentStartDate: new Date("2025-01-05"),
|
||||||
|
currentEndDate: new Date("2025-01-07"),
|
||||||
|
baseMutationAllocationId: "alloc-1",
|
||||||
|
requiresExtraction: true,
|
||||||
|
pendingSnapshot: {
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: "alloc-1",
|
||||||
|
projectName: "Project One",
|
||||||
|
before: {
|
||||||
|
startDate: new Date("2025-01-01"),
|
||||||
|
endDate: new Date("2025-01-03"),
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
startDate: new Date("2025-01-05"),
|
||||||
|
endDate: new Date("2025-01-07"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
previewOps: { preserve, clear },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preserve).toHaveBeenCalledOnce();
|
||||||
|
expect(clear).toHaveBeenCalledOnce();
|
||||||
|
expect(pendingOptimisticAllocationIdRef.current).toBe("alloc-1");
|
||||||
|
expect(optimisticHarness.value.get("alloc-1")).toEqual({
|
||||||
|
startDate: new Date("2025-01-05"),
|
||||||
|
endDate: new Date("2025-01-07"),
|
||||||
|
});
|
||||||
|
expect(extractAllocationFragment).toHaveBeenCalledWith({
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
startDate: new Date("2025-01-01"),
|
||||||
|
endDate: new Date("2025-01-03"),
|
||||||
|
});
|
||||||
|
expect(pendingSnapshotRef.current).toEqual(
|
||||||
|
expect.objectContaining({ mutationAllocationId: "alloc-fragment-2" }),
|
||||||
|
);
|
||||||
|
expect(updateAllocation).toHaveBeenCalledWith({
|
||||||
|
allocationId: "alloc-fragment-2",
|
||||||
|
startDate: new Date("2025-01-05"),
|
||||||
|
endDate: new Date("2025-01-07"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears optimistic state when extraction fails", async () => {
|
||||||
|
const optimisticHarness = createOptimisticStateHarness();
|
||||||
|
const pendingSnapshotRef = {
|
||||||
|
current: null as {
|
||||||
|
allocationId: string;
|
||||||
|
mutationAllocationId: string;
|
||||||
|
projectName: string;
|
||||||
|
before: { startDate: Date; endDate: Date };
|
||||||
|
after: { startDate: Date; endDate: Date };
|
||||||
|
} | null,
|
||||||
|
};
|
||||||
|
const pendingOptimisticAllocationIdRef = { current: null as string | null };
|
||||||
|
const clearPendingOptimisticAllocation = vi.fn((allocationId: string) => {
|
||||||
|
pendingSnapshotRef.current = null;
|
||||||
|
pendingOptimisticAllocationIdRef.current = null;
|
||||||
|
optimisticHarness.setOptimisticAllocations((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(allocationId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const updateAllocation = vi.fn();
|
||||||
|
|
||||||
|
await finalizeAllocationReleaseEffects({
|
||||||
|
clientX: 52,
|
||||||
|
allocRef: { current: BASE_ALLOC },
|
||||||
|
previewRef: { current: {} as never },
|
||||||
|
updatePosition: vi.fn(),
|
||||||
|
clickThresholdPx: 5,
|
||||||
|
wasShift: false,
|
||||||
|
pendingSnapshotRef,
|
||||||
|
pendingOptimisticAllocationIdRef,
|
||||||
|
setOptimisticAllocations: optimisticHarness.setOptimisticAllocations,
|
||||||
|
extractAllocationFragment: vi.fn().mockRejectedValue(new Error("boom")),
|
||||||
|
updateAllocation,
|
||||||
|
clearPendingOptimisticAllocation,
|
||||||
|
resolveRelease: () => ({
|
||||||
|
kind: "mutation",
|
||||||
|
preservePreview: true,
|
||||||
|
mutationPlan: {
|
||||||
|
activeAllocationId: "alloc-1",
|
||||||
|
currentStartDate: new Date("2025-01-05"),
|
||||||
|
currentEndDate: new Date("2025-01-07"),
|
||||||
|
baseMutationAllocationId: "alloc-1",
|
||||||
|
requiresExtraction: true,
|
||||||
|
pendingSnapshot: {
|
||||||
|
allocationId: "alloc-1",
|
||||||
|
mutationAllocationId: "alloc-1",
|
||||||
|
projectName: "Project One",
|
||||||
|
before: {
|
||||||
|
startDate: new Date("2025-01-01"),
|
||||||
|
endDate: new Date("2025-01-03"),
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
startDate: new Date("2025-01-05"),
|
||||||
|
endDate: new Date("2025-01-07"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
previewOps: { preserve: vi.fn(), clear: vi.fn() },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(clearPendingOptimisticAllocation).toHaveBeenCalledWith("alloc-1");
|
||||||
|
expect(updateAllocation).not.toHaveBeenCalled();
|
||||||
|
expect(pendingSnapshotRef.current).toBeNull();
|
||||||
|
expect(pendingOptimisticAllocationIdRef.current).toBeNull();
|
||||||
|
expect(optimisticHarness.value.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { type AllocationMovedSnapshotLike } from "./timelineAllocationFinalize.js";
|
||||||
|
import { clearLivePreview, preserveLivePreview, type LivePreviewSession } from "./timelineLivePreview.js";
|
||||||
|
import { resolveAllocationRelease, type AllocationReleaseOutcome } from "./timelineAllocationRelease.js";
|
||||||
|
|
||||||
|
type MutableCurrent<T> = { current: T };
|
||||||
|
type AllocationDragReleaseLike = Parameters<typeof resolveAllocationRelease>[0];
|
||||||
|
type AllocationBlockClickInfo = Extract<AllocationReleaseOutcome, { kind: "click" }>["clickInfo"];
|
||||||
|
type OptimisticAllocationOverride = { startDate: Date; endDate: Date };
|
||||||
|
type SetOptimisticAllocations = (
|
||||||
|
updater: (prev: Map<string, OptimisticAllocationOverride>) => Map<string, OptimisticAllocationOverride>,
|
||||||
|
) => void;
|
||||||
|
type MutationInput = { allocationId: string; startDate: Date; endDate: Date };
|
||||||
|
type PreviewOps = {
|
||||||
|
preserve: (preview: LivePreviewSession | null) => void;
|
||||||
|
clear: (preview: LivePreviewSession | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FinalizeAllocationReleaseEffectsParams = {
|
||||||
|
clientX: number;
|
||||||
|
allocRef: MutableCurrent<AllocationDragReleaseLike>;
|
||||||
|
previewRef: MutableCurrent<LivePreviewSession | null>;
|
||||||
|
updatePosition: (clientX: number) => void;
|
||||||
|
clickThresholdPx: number;
|
||||||
|
wasShift: boolean;
|
||||||
|
onShiftClick?: ((allocationId: string) => void) | undefined;
|
||||||
|
onBlockClick?: ((clickInfo: AllocationBlockClickInfo) => void) | undefined;
|
||||||
|
pendingSnapshotRef: MutableCurrent<AllocationMovedSnapshotLike | null>;
|
||||||
|
pendingOptimisticAllocationIdRef: MutableCurrent<string | null>;
|
||||||
|
setOptimisticAllocations: SetOptimisticAllocations;
|
||||||
|
extractAllocationFragment: (input: MutationInput) => Promise<{ extractedAllocationId: string }>;
|
||||||
|
updateAllocation: (input: MutationInput) => void;
|
||||||
|
clearPendingOptimisticAllocation: (allocationId: string) => void;
|
||||||
|
resolveRelease?: (
|
||||||
|
alloc: AllocationDragReleaseLike,
|
||||||
|
options: { clickThresholdPx: number; wasShift: boolean },
|
||||||
|
) => AllocationReleaseOutcome;
|
||||||
|
previewOps?: PreviewOps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function finalizeAllocationReleaseEffects({
|
||||||
|
clientX,
|
||||||
|
allocRef,
|
||||||
|
previewRef,
|
||||||
|
updatePosition,
|
||||||
|
clickThresholdPx,
|
||||||
|
wasShift,
|
||||||
|
onShiftClick,
|
||||||
|
onBlockClick,
|
||||||
|
pendingSnapshotRef,
|
||||||
|
pendingOptimisticAllocationIdRef,
|
||||||
|
setOptimisticAllocations,
|
||||||
|
extractAllocationFragment,
|
||||||
|
updateAllocation,
|
||||||
|
clearPendingOptimisticAllocation,
|
||||||
|
resolveRelease = resolveAllocationRelease,
|
||||||
|
previewOps = { preserve: preserveLivePreview, clear: clearLivePreview },
|
||||||
|
}: FinalizeAllocationReleaseEffectsParams): Promise<void> {
|
||||||
|
updatePosition(clientX);
|
||||||
|
const alloc = allocRef.current;
|
||||||
|
const release = resolveRelease(alloc, { clickThresholdPx, wasShift });
|
||||||
|
if (release.kind === "ignore") return;
|
||||||
|
if (release.preservePreview) previewOps.preserve(previewRef.current);
|
||||||
|
previewOps.clear(previewRef.current);
|
||||||
|
previewRef.current = null;
|
||||||
|
if (release.kind === "shift-click") return void onShiftClick?.(release.allocationId);
|
||||||
|
if (release.kind === "click") return void onBlockClick?.(release.clickInfo);
|
||||||
|
if (release.kind !== "mutation") return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeAllocationId,
|
||||||
|
currentStartDate,
|
||||||
|
currentEndDate,
|
||||||
|
baseMutationAllocationId,
|
||||||
|
requiresExtraction,
|
||||||
|
pendingSnapshot,
|
||||||
|
} = release.mutationPlan;
|
||||||
|
|
||||||
|
pendingSnapshotRef.current = pendingSnapshot;
|
||||||
|
pendingOptimisticAllocationIdRef.current = activeAllocationId;
|
||||||
|
setOptimisticAllocations((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(activeAllocationId, { startDate: currentStartDate, endDate: currentEndDate });
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
let mutationAllocationId = baseMutationAllocationId;
|
||||||
|
if (requiresExtraction) {
|
||||||
|
if (!alloc.originalStartDate || !alloc.originalEndDate) throw new Error("missing original allocation dates");
|
||||||
|
const extracted = await extractAllocationFragment({
|
||||||
|
allocationId: mutationAllocationId,
|
||||||
|
startDate: alloc.originalStartDate,
|
||||||
|
endDate: alloc.originalEndDate,
|
||||||
|
});
|
||||||
|
mutationAllocationId = extracted.extractedAllocationId;
|
||||||
|
}
|
||||||
|
pendingSnapshotRef.current = pendingSnapshotRef.current
|
||||||
|
? { ...pendingSnapshotRef.current, mutationAllocationId }
|
||||||
|
: null;
|
||||||
|
updateAllocation({ allocationId: mutationAllocationId, startDate: currentStartDate, endDate: currentEndDate });
|
||||||
|
} catch {
|
||||||
|
clearPendingOptimisticAllocation(activeAllocationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
scheduleLivePreview,
|
scheduleLivePreview,
|
||||||
type LivePreviewSession,
|
type LivePreviewSession,
|
||||||
} from "./timelineLivePreview.js";
|
} from "./timelineLivePreview.js";
|
||||||
import { buildAllocationMovedSnapshot } from "./timelineAllocationFinalize.js";
|
|
||||||
import {
|
import {
|
||||||
finalizeAllocationMultiDrag,
|
finalizeAllocationMultiDrag,
|
||||||
isAllocationMultiSelected,
|
isAllocationMultiSelected,
|
||||||
@@ -19,9 +18,9 @@ import {
|
|||||||
updateAllocationMultiDrag,
|
updateAllocationMultiDrag,
|
||||||
} from "./timelineAllocationMultiDrag.js";
|
} from "./timelineAllocationMultiDrag.js";
|
||||||
import { beginAllocationMultiDragSession } from "./timelineAllocationMultiDragSession.js";
|
import { beginAllocationMultiDragSession } from "./timelineAllocationMultiDragSession.js";
|
||||||
import { resolveAllocationRelease } from "./timelineAllocationRelease.js";
|
|
||||||
import { createAllocationDragState } from "./timelineAllocationDragState.js";
|
import { createAllocationDragState } from "./timelineAllocationDragState.js";
|
||||||
import { beginAllocationDragSession } from "./timelineAllocationDragSession.js";
|
import { beginAllocationDragSession } from "./timelineAllocationDragSession.js";
|
||||||
|
import { finalizeAllocationReleaseEffects } from "./timelineAllocationReleaseEffects.js";
|
||||||
import { cleanupTimelineDragState } from "./timelineDragCleanup.js";
|
import { cleanupTimelineDragState } from "./timelineDragCleanup.js";
|
||||||
import { resolveAllocationDragPosition, resolveProjectDragPosition } from "./timelineDragPosition.js";
|
import { resolveAllocationDragPosition, resolveProjectDragPosition } from "./timelineDragPosition.js";
|
||||||
import { attachDocumentMouseDrag } from "./timelineDocumentDrag.js";
|
import { attachDocumentMouseDrag } from "./timelineDocumentDrag.js";
|
||||||
@@ -640,75 +639,22 @@ export function useTimelineDrag({
|
|||||||
attachDrag: attachDocumentMouseDrag,
|
attachDrag: attachDocumentMouseDrag,
|
||||||
updatePosition: updateAllocationDragPosition,
|
updatePosition: updateAllocationDragPosition,
|
||||||
finalize: (clientX) => {
|
finalize: (clientX) => {
|
||||||
updateAllocationDragPosition(clientX);
|
void finalizeAllocationReleaseEffects({
|
||||||
const alloc = allocDragRef.current;
|
clientX,
|
||||||
const release = resolveAllocationRelease(alloc, {
|
allocRef: allocDragRef,
|
||||||
|
previewRef: allocPreviewRef,
|
||||||
|
updatePosition: updateAllocationDragPosition,
|
||||||
clickThresholdPx: DRAG_CLICK_THRESHOLD_PX,
|
clickThresholdPx: DRAG_CLICK_THRESHOLD_PX,
|
||||||
wasShift,
|
wasShift,
|
||||||
|
onShiftClick: onShiftClickAllocRef.current,
|
||||||
|
onBlockClick: onBlockClickRef.current,
|
||||||
|
pendingSnapshotRef,
|
||||||
|
pendingOptimisticAllocationIdRef,
|
||||||
|
setOptimisticAllocations,
|
||||||
|
extractAllocationFragment: extractAllocFragmentMutation.mutateAsync,
|
||||||
|
updateAllocation: updateAllocMutation.mutate,
|
||||||
|
clearPendingOptimisticAllocation,
|
||||||
});
|
});
|
||||||
if (release.kind === "ignore") return;
|
|
||||||
|
|
||||||
if (release.preservePreview) {
|
|
||||||
preserveLivePreview(allocPreviewRef.current);
|
|
||||||
}
|
|
||||||
clearLivePreview(allocPreviewRef.current);
|
|
||||||
allocPreviewRef.current = null;
|
|
||||||
|
|
||||||
if (release.kind === "shift-click") {
|
|
||||||
onShiftClickAllocRef.current?.(release.allocationId);
|
|
||||||
} else if (release.kind === "click") {
|
|
||||||
onBlockClickRef.current?.(release.clickInfo);
|
|
||||||
} else if (release.kind === "mutation") {
|
|
||||||
const { mutationPlan } = release;
|
|
||||||
const {
|
|
||||||
activeAllocationId,
|
|
||||||
currentStartDate,
|
|
||||||
currentEndDate,
|
|
||||||
baseMutationAllocationId,
|
|
||||||
requiresExtraction,
|
|
||||||
pendingSnapshot,
|
|
||||||
} = mutationPlan;
|
|
||||||
|
|
||||||
pendingSnapshotRef.current = pendingSnapshot;
|
|
||||||
pendingOptimisticAllocationIdRef.current = activeAllocationId;
|
|
||||||
setOptimisticAllocations((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(activeAllocationId, {
|
|
||||||
startDate: currentStartDate,
|
|
||||||
endDate: currentEndDate,
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
let mutationAllocationId = baseMutationAllocationId;
|
|
||||||
|
|
||||||
if (requiresExtraction) {
|
|
||||||
const extracted = await extractAllocFragmentMutation.mutateAsync({
|
|
||||||
allocationId: mutationAllocationId,
|
|
||||||
startDate: alloc.originalStartDate!,
|
|
||||||
endDate: alloc.originalEndDate!,
|
|
||||||
});
|
|
||||||
mutationAllocationId = extracted.extractedAllocationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingSnapshotRef.current = pendingSnapshotRef.current
|
|
||||||
? {
|
|
||||||
...pendingSnapshotRef.current,
|
|
||||||
mutationAllocationId,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
updateAllocMutation.mutate({
|
|
||||||
allocationId: mutationAllocationId,
|
|
||||||
startDate: currentStartDate,
|
|
||||||
endDate: currentEndDate,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
clearPendingOptimisticAllocation(activeAllocationId);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
allocDragRef.current = INITIAL_ALLOC_DRAG;
|
allocDragRef.current = INITIAL_ALLOC_DRAG;
|
||||||
setAllocDragState(INITIAL_ALLOC_DRAG);
|
setAllocDragState(INITIAL_ALLOC_DRAG);
|
||||||
|
|||||||
@@ -351,6 +351,25 @@ export const rules = [
|
|||||||
],
|
],
|
||||||
forbidden: [],
|
forbidden: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
file: "apps/web/src/hooks/timelineAllocationReleaseEffects.ts",
|
||||||
|
maxLines: 130,
|
||||||
|
required: [
|
||||||
|
{
|
||||||
|
pattern: /\bexport async function finalizeAllocationReleaseEffects\b/,
|
||||||
|
message: "timeline allocation release effect helpers must keep release side effects centralized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /from "\.\/timelineAllocationRelease\.js"/,
|
||||||
|
message: "timeline allocation release effect helpers must keep release classification delegated to the extracted helper module",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /from "\.\/timelineLivePreview\.js"/,
|
||||||
|
message: "timeline allocation release effect helpers must keep preview lifecycle delegated to the extracted helper module",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
forbidden: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
file: "apps/web/src/hooks/timelineProjectDrag.ts",
|
file: "apps/web/src/hooks/timelineProjectDrag.ts",
|
||||||
maxLines: 80,
|
maxLines: 80,
|
||||||
@@ -408,14 +427,6 @@ export const rules = [
|
|||||||
pattern: /from "\.\/timelineOptimisticAllocations\.js"/,
|
pattern: /from "\.\/timelineOptimisticAllocations\.js"/,
|
||||||
message: "timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
message: "timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pattern: /from "\.\/timelineAllocationFinalize\.js"/,
|
|
||||||
message: "timeline drag must keep allocation drag completion rules delegated to the extracted helper module",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: /from "\.\/timelineAllocationRelease\.js"/,
|
|
||||||
message: "timeline drag must keep allocation release classification delegated to the extracted helper module",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
pattern: /from "\.\/timelineDragCleanup\.js"/,
|
pattern: /from "\.\/timelineDragCleanup\.js"/,
|
||||||
message: "timeline drag must keep unmount teardown delegated to the extracted helper module",
|
message: "timeline drag must keep unmount teardown delegated to the extracted helper module",
|
||||||
@@ -444,6 +455,10 @@ export const rules = [
|
|||||||
pattern: /from "\.\/timelineAllocationDragSession\.js"/,
|
pattern: /from "\.\/timelineAllocationDragSession\.js"/,
|
||||||
message: "timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
message: "timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /from "\.\/timelineAllocationReleaseEffects\.js"/,
|
||||||
|
message: "timeline drag must keep allocation release side effects delegated to the extracted helper module",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /from "\.\/timelineProjectDrag\.js"/,
|
pattern: /from "\.\/timelineProjectDrag\.js"/,
|
||||||
message: "timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
message: "timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
||||||
@@ -502,6 +517,10 @@ export const rules = [
|
|||||||
pattern: /\bfunction handle(?:Move|Up)\b/,
|
pattern: /\bfunction handle(?:Move|Up)\b/,
|
||||||
message: "timeline drag must not re-inline extracted allocation drag session handlers",
|
message: "timeline drag must not re-inline extracted allocation drag session handlers",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /\bpendingSnapshotRef\.current = pendingSnapshot\b[\s\S]*updateAllocMutation\.mutate\(/,
|
||||||
|
message: "timeline drag must not re-inline extracted allocation release effect mutation wiring",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /\bfunction (?:createProjectDragState|buildProjectShiftMutationInput)\b/,
|
pattern: /\bfunction (?:createProjectDragState|buildProjectShiftMutationInput)\b/,
|
||||||
message: "timeline drag must not re-inline extracted project drag helper implementations",
|
message: "timeline drag must not re-inline extracted project drag helper implementations",
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ describe("architecture guardrails", () => {
|
|||||||
const documentDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineDocumentDrag.ts");
|
const documentDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineDocumentDrag.ts");
|
||||||
const allocationDragStateRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationDragState.ts");
|
const allocationDragStateRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationDragState.ts");
|
||||||
const allocationDragSessionRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationDragSession.ts");
|
const allocationDragSessionRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationDragSession.ts");
|
||||||
|
const allocationReleaseEffectsRule = rules.find(
|
||||||
|
(rule) => rule.file === "apps/web/src/hooks/timelineAllocationReleaseEffects.ts",
|
||||||
|
);
|
||||||
const projectDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineProjectDrag.ts");
|
const projectDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineProjectDrag.ts");
|
||||||
const projectDragSessionRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineProjectDragSession.ts");
|
const projectDragSessionRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineProjectDragSession.ts");
|
||||||
|
|
||||||
@@ -110,6 +113,7 @@ describe("architecture guardrails", () => {
|
|||||||
assert.ok(documentDragRule);
|
assert.ok(documentDragRule);
|
||||||
assert.ok(allocationDragStateRule);
|
assert.ok(allocationDragStateRule);
|
||||||
assert.ok(allocationDragSessionRule);
|
assert.ok(allocationDragSessionRule);
|
||||||
|
assert.ok(allocationReleaseEffectsRule);
|
||||||
assert.ok(projectDragRule);
|
assert.ok(projectDragRule);
|
||||||
assert.ok(projectDragSessionRule);
|
assert.ok(projectDragSessionRule);
|
||||||
|
|
||||||
@@ -121,8 +125,6 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep multi-select document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep multi-select document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep range preview and finalization delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep range preview and finalization delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag completion rules delegated to the extracted helper module",
|
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation release classification delegated to the extracted helper module",
|
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep unmount teardown delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep unmount teardown delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project and allocation drag position derivation delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project and allocation drag position derivation delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
||||||
@@ -130,6 +132,7 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag bootstrap delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag bootstrap delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
||||||
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation release side effects delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: forbidden pattern matched: timeline drag must not re-inline live preview helper implementations",
|
"apps/web/src/hooks/useTimelineDrag.ts: forbidden pattern matched: timeline drag must not re-inline live preview helper implementations",
|
||||||
@@ -211,6 +214,12 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/timelineAllocationDragSession.ts: missing guardrail anchor: timeline allocation drag session helpers must keep document drag lifecycle centralized",
|
"apps/web/src/hooks/timelineAllocationDragSession.ts: missing guardrail anchor: timeline allocation drag session helpers must keep document drag lifecycle centralized",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(evaluateRule(allocationReleaseEffectsRule, ""), [
|
||||||
|
"apps/web/src/hooks/timelineAllocationReleaseEffects.ts: missing guardrail anchor: timeline allocation release effect helpers must keep release side effects centralized",
|
||||||
|
"apps/web/src/hooks/timelineAllocationReleaseEffects.ts: missing guardrail anchor: timeline allocation release effect helpers must keep release classification delegated to the extracted helper module",
|
||||||
|
"apps/web/src/hooks/timelineAllocationReleaseEffects.ts: missing guardrail anchor: timeline allocation release effect helpers must keep preview lifecycle delegated to the extracted helper module",
|
||||||
|
]);
|
||||||
|
|
||||||
assert.deepEqual(evaluateRule(projectDragRule, "export function createProjectDragState() {}\n"), [
|
assert.deepEqual(evaluateRule(projectDragRule, "export function createProjectDragState() {}\n"), [
|
||||||
"apps/web/src/hooks/timelineProjectDrag.ts: missing guardrail anchor: timeline project drag helpers must keep no-op project-shift mutation gating centralized",
|
"apps/web/src/hooks/timelineProjectDrag.ts: missing guardrail anchor: timeline project drag helpers must keep no-op project-shift mutation gating centralized",
|
||||||
]);
|
]);
|
||||||
@@ -231,8 +240,6 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep multi-select document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep multi-select document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep range preview and finalization delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep range preview and finalization delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep optimistic allocation reconciliation delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag completion rules delegated to the extracted helper module",
|
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation release classification delegated to the extracted helper module",
|
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep unmount teardown delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep unmount teardown delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project and allocation drag position derivation delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project and allocation drag position derivation delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
||||||
@@ -240,6 +247,7 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag bootstrap delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag bootstrap delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation drag document session wiring delegated to the extracted helper module",
|
||||||
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation release side effects delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag document session wiring delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep project drag document session wiring delegated to the extracted helper module",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: forbidden pattern matched: timeline drag must not re-inline synthetic touch pointer adapters",
|
"apps/web/src/hooks/useTimelineDrag.ts: forbidden pattern matched: timeline drag must not re-inline synthetic touch pointer adapters",
|
||||||
|
|||||||
Reference in New Issue
Block a user