refactor(web): extract timeline drag cleanup
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { cleanupTimelineDragState } from "./timelineDragCleanup.js";
|
||||||
|
|
||||||
|
describe("timelineDragCleanup", () => {
|
||||||
|
it("runs registered cleanup callbacks, clears previews, and resets refs", () => {
|
||||||
|
const projectCleanup = vi.fn();
|
||||||
|
const allocCleanup = vi.fn();
|
||||||
|
const multiCleanup = vi.fn();
|
||||||
|
const projectPreview = { id: "project-preview" };
|
||||||
|
const allocPreview = { id: "alloc-preview" };
|
||||||
|
const clearPreview = vi.fn();
|
||||||
|
|
||||||
|
const projectDragCleanupRef = { current: projectCleanup };
|
||||||
|
const allocDragCleanupRef = { current: allocCleanup };
|
||||||
|
const multiSelectCleanupRef = { current: multiCleanup };
|
||||||
|
const projectPreviewRef = { current: projectPreview };
|
||||||
|
const allocPreviewRef = { current: allocPreview };
|
||||||
|
const dragStateRef = { current: { drag: true } };
|
||||||
|
const allocDragRef = { current: { alloc: true } };
|
||||||
|
const rangeStateRef = { current: { range: true } };
|
||||||
|
const multiSelectRef = { current: { multi: true } };
|
||||||
|
|
||||||
|
const initialDragState = { drag: false };
|
||||||
|
const initialAllocDragState = { alloc: false };
|
||||||
|
const initialRangeState = { range: false };
|
||||||
|
const initialMultiSelectState = { multi: false };
|
||||||
|
|
||||||
|
cleanupTimelineDragState({
|
||||||
|
projectDragCleanupRef,
|
||||||
|
allocDragCleanupRef,
|
||||||
|
multiSelectCleanupRef,
|
||||||
|
projectPreviewRef,
|
||||||
|
allocPreviewRef,
|
||||||
|
dragStateRef,
|
||||||
|
allocDragRef,
|
||||||
|
rangeStateRef,
|
||||||
|
multiSelectRef,
|
||||||
|
initialDragState,
|
||||||
|
initialAllocDragState,
|
||||||
|
initialRangeState,
|
||||||
|
initialMultiSelectState,
|
||||||
|
clearPreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(projectCleanup).toHaveBeenCalledOnce();
|
||||||
|
expect(allocCleanup).toHaveBeenCalledOnce();
|
||||||
|
expect(multiCleanup).toHaveBeenCalledOnce();
|
||||||
|
expect(clearPreview).toHaveBeenNthCalledWith(1, projectPreview);
|
||||||
|
expect(clearPreview).toHaveBeenNthCalledWith(2, allocPreview);
|
||||||
|
expect(projectDragCleanupRef.current).toBeNull();
|
||||||
|
expect(allocDragCleanupRef.current).toBeNull();
|
||||||
|
expect(multiSelectCleanupRef.current).toBeNull();
|
||||||
|
expect(projectPreviewRef.current).toBeNull();
|
||||||
|
expect(allocPreviewRef.current).toBeNull();
|
||||||
|
expect(dragStateRef.current).toBe(initialDragState);
|
||||||
|
expect(allocDragRef.current).toBe(initialAllocDragState);
|
||||||
|
expect(rangeStateRef.current).toBe(initialRangeState);
|
||||||
|
expect(multiSelectRef.current).toBe(initialMultiSelectState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tolerates missing cleanups and preview sessions", () => {
|
||||||
|
const clearPreview = vi.fn();
|
||||||
|
|
||||||
|
cleanupTimelineDragState({
|
||||||
|
projectDragCleanupRef: { current: null },
|
||||||
|
allocDragCleanupRef: { current: null },
|
||||||
|
multiSelectCleanupRef: { current: null },
|
||||||
|
projectPreviewRef: { current: null },
|
||||||
|
allocPreviewRef: { current: null },
|
||||||
|
dragStateRef: { current: { drag: true } },
|
||||||
|
allocDragRef: { current: { alloc: true } },
|
||||||
|
rangeStateRef: { current: { range: true } },
|
||||||
|
multiSelectRef: { current: { multi: true } },
|
||||||
|
initialDragState: { drag: false },
|
||||||
|
initialAllocDragState: { alloc: false },
|
||||||
|
initialRangeState: { range: false },
|
||||||
|
initialMultiSelectState: { multi: false },
|
||||||
|
clearPreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(clearPreview).toHaveBeenCalledTimes(2);
|
||||||
|
expect(clearPreview).toHaveBeenNthCalledWith(1, null);
|
||||||
|
expect(clearPreview).toHaveBeenNthCalledWith(2, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
type MutableCurrent<T> = {
|
||||||
|
current: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TimelineDragCleanupParams<
|
||||||
|
DragState,
|
||||||
|
AllocDragState,
|
||||||
|
RangeState,
|
||||||
|
MultiSelectState,
|
||||||
|
PreviewSession,
|
||||||
|
> = {
|
||||||
|
projectDragCleanupRef: MutableCurrent<(() => void) | null>;
|
||||||
|
allocDragCleanupRef: MutableCurrent<(() => void) | null>;
|
||||||
|
multiSelectCleanupRef: MutableCurrent<(() => void) | null>;
|
||||||
|
projectPreviewRef: MutableCurrent<PreviewSession | null>;
|
||||||
|
allocPreviewRef: MutableCurrent<PreviewSession | null>;
|
||||||
|
dragStateRef: MutableCurrent<DragState>;
|
||||||
|
allocDragRef: MutableCurrent<AllocDragState>;
|
||||||
|
rangeStateRef: MutableCurrent<RangeState>;
|
||||||
|
multiSelectRef: MutableCurrent<MultiSelectState>;
|
||||||
|
initialDragState: DragState;
|
||||||
|
initialAllocDragState: AllocDragState;
|
||||||
|
initialRangeState: RangeState;
|
||||||
|
initialMultiSelectState: MultiSelectState;
|
||||||
|
clearPreview: (session: PreviewSession | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cleanupTimelineDragState<
|
||||||
|
DragState,
|
||||||
|
AllocDragState,
|
||||||
|
RangeState,
|
||||||
|
MultiSelectState,
|
||||||
|
PreviewSession,
|
||||||
|
>({
|
||||||
|
projectDragCleanupRef,
|
||||||
|
allocDragCleanupRef,
|
||||||
|
multiSelectCleanupRef,
|
||||||
|
projectPreviewRef,
|
||||||
|
allocPreviewRef,
|
||||||
|
dragStateRef,
|
||||||
|
allocDragRef,
|
||||||
|
rangeStateRef,
|
||||||
|
multiSelectRef,
|
||||||
|
initialDragState,
|
||||||
|
initialAllocDragState,
|
||||||
|
initialRangeState,
|
||||||
|
initialMultiSelectState,
|
||||||
|
clearPreview,
|
||||||
|
}: TimelineDragCleanupParams<
|
||||||
|
DragState,
|
||||||
|
AllocDragState,
|
||||||
|
RangeState,
|
||||||
|
MultiSelectState,
|
||||||
|
PreviewSession
|
||||||
|
>) {
|
||||||
|
projectDragCleanupRef.current?.();
|
||||||
|
allocDragCleanupRef.current?.();
|
||||||
|
multiSelectCleanupRef.current?.();
|
||||||
|
|
||||||
|
projectDragCleanupRef.current = null;
|
||||||
|
allocDragCleanupRef.current = null;
|
||||||
|
multiSelectCleanupRef.current = null;
|
||||||
|
|
||||||
|
clearPreview(projectPreviewRef.current);
|
||||||
|
clearPreview(allocPreviewRef.current);
|
||||||
|
|
||||||
|
projectPreviewRef.current = null;
|
||||||
|
allocPreviewRef.current = null;
|
||||||
|
|
||||||
|
dragStateRef.current = initialDragState;
|
||||||
|
allocDragRef.current = initialAllocDragState;
|
||||||
|
rangeStateRef.current = initialRangeState;
|
||||||
|
multiSelectRef.current = initialMultiSelectState;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "./timelineAllocationMultiDrag.js";
|
} from "./timelineAllocationMultiDrag.js";
|
||||||
import { resolveAllocationRelease } from "./timelineAllocationRelease.js";
|
import { resolveAllocationRelease } from "./timelineAllocationRelease.js";
|
||||||
import { createAllocationDragState } from "./timelineAllocationDragState.js";
|
import { createAllocationDragState } from "./timelineAllocationDragState.js";
|
||||||
|
import { cleanupTimelineDragState } from "./timelineDragCleanup.js";
|
||||||
import { attachDocumentMouseDrag } from "./timelineDocumentDrag.js";
|
import { attachDocumentMouseDrag } from "./timelineDocumentDrag.js";
|
||||||
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
|
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
|
||||||
import {
|
import {
|
||||||
@@ -1005,20 +1006,22 @@ export function useTimelineDrag({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
projectDragCleanupRef.current?.();
|
cleanupTimelineDragState({
|
||||||
allocDragCleanupRef.current?.();
|
projectDragCleanupRef,
|
||||||
multiSelectCleanupRef.current?.();
|
allocDragCleanupRef,
|
||||||
projectDragCleanupRef.current = null;
|
multiSelectCleanupRef,
|
||||||
allocDragCleanupRef.current = null;
|
projectPreviewRef,
|
||||||
multiSelectCleanupRef.current = null;
|
allocPreviewRef,
|
||||||
clearLivePreview(projectPreviewRef.current);
|
dragStateRef,
|
||||||
clearLivePreview(allocPreviewRef.current);
|
allocDragRef,
|
||||||
projectPreviewRef.current = null;
|
rangeStateRef,
|
||||||
allocPreviewRef.current = null;
|
multiSelectRef,
|
||||||
dragStateRef.current = INITIAL_DRAG_STATE;
|
initialDragState: INITIAL_DRAG_STATE,
|
||||||
allocDragRef.current = INITIAL_ALLOC_DRAG;
|
initialAllocDragState: INITIAL_ALLOC_DRAG,
|
||||||
rangeStateRef.current = INITIAL_RANGE_STATE;
|
initialRangeState: INITIAL_RANGE_STATE,
|
||||||
multiSelectRef.current = INITIAL_MULTI_SELECT;
|
initialMultiSelectState: INITIAL_MULTI_SELECT,
|
||||||
|
clearPreview: clearLivePreview,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,17 @@ export const rules = [
|
|||||||
],
|
],
|
||||||
forbidden: [],
|
forbidden: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
file: "apps/web/src/hooks/timelineDragCleanup.ts",
|
||||||
|
maxLines: 80,
|
||||||
|
required: [
|
||||||
|
{
|
||||||
|
pattern: /\bexport function cleanupTimelineDragState\b/,
|
||||||
|
message: "timeline drag cleanup helpers must keep unmount teardown centralized",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
forbidden: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
file: "apps/web/src/hooks/timelineDocumentDrag.ts",
|
file: "apps/web/src/hooks/timelineDocumentDrag.ts",
|
||||||
maxLines: 50,
|
maxLines: 50,
|
||||||
@@ -342,6 +353,10 @@ export const rules = [
|
|||||||
pattern: /from "\.\/timelineAllocationRelease\.js"/,
|
pattern: /from "\.\/timelineAllocationRelease\.js"/,
|
||||||
message: "timeline drag must keep allocation release classification delegated to the extracted helper module",
|
message: "timeline drag must keep allocation release classification delegated to the extracted helper module",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /from "\.\/timelineDragCleanup\.js"/,
|
||||||
|
message: "timeline drag must keep unmount teardown delegated to the extracted helper module",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /from "\.\/timelineDocumentDrag\.js"/,
|
pattern: /from "\.\/timelineDocumentDrag\.js"/,
|
||||||
message: "timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
message: "timeline drag must keep document mouse listener lifecycle delegated to the extracted helper module",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ describe("architecture guardrails", () => {
|
|||||||
const allocationMultiDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationMultiDrag.ts");
|
const allocationMultiDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationMultiDrag.ts");
|
||||||
const allocationActionsRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationActions.ts");
|
const allocationActionsRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationActions.ts");
|
||||||
const allocationReleaseRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationRelease.ts");
|
const allocationReleaseRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationRelease.ts");
|
||||||
|
const cleanupRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineDragCleanup.ts");
|
||||||
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 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");
|
||||||
@@ -95,6 +96,7 @@ describe("architecture guardrails", () => {
|
|||||||
assert.ok(allocationMultiDragRule);
|
assert.ok(allocationMultiDragRule);
|
||||||
assert.ok(allocationActionsRule);
|
assert.ok(allocationActionsRule);
|
||||||
assert.ok(allocationReleaseRule);
|
assert.ok(allocationReleaseRule);
|
||||||
|
assert.ok(cleanupRule);
|
||||||
assert.ok(documentDragRule);
|
assert.ok(documentDragRule);
|
||||||
assert.ok(allocationDragStateRule);
|
assert.ok(allocationDragStateRule);
|
||||||
assert.ok(projectDragRule);
|
assert.ok(projectDragRule);
|
||||||
@@ -108,6 +110,7 @@ describe("architecture guardrails", () => {
|
|||||||
"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 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 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 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",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag rules delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag rules 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",
|
||||||
@@ -163,6 +166,10 @@ describe("architecture guardrails", () => {
|
|||||||
"apps/web/src/hooks/timelineAllocationRelease.ts: missing guardrail anchor: timeline allocation release helpers must keep click and mutation plan derivation delegated to allocation action helpers",
|
"apps/web/src/hooks/timelineAllocationRelease.ts: missing guardrail anchor: timeline allocation release helpers must keep click and mutation plan derivation delegated to allocation action helpers",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(evaluateRule(cleanupRule, ""), [
|
||||||
|
"apps/web/src/hooks/timelineDragCleanup.ts: missing guardrail anchor: timeline drag cleanup helpers must keep unmount teardown centralized",
|
||||||
|
]);
|
||||||
|
|
||||||
assert.deepEqual(evaluateRule(documentDragRule, ""), [
|
assert.deepEqual(evaluateRule(documentDragRule, ""), [
|
||||||
"apps/web/src/hooks/timelineDocumentDrag.ts: missing guardrail anchor: timeline document drag helpers must keep document mouse listener wiring centralized",
|
"apps/web/src/hooks/timelineDocumentDrag.ts: missing guardrail anchor: timeline document drag helpers must keep document mouse listener wiring centralized",
|
||||||
]);
|
]);
|
||||||
@@ -188,6 +195,7 @@ describe("architecture guardrails", () => {
|
|||||||
"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 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 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 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",
|
||||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag rules delegated to the extracted helper module",
|
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep allocation multi-drag rules 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",
|
||||||
|
|||||||
Reference in New Issue
Block a user