refactor(web): extract project drag helpers
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
|
||||
|
||||
type TestProjectDragState = {
|
||||
isDragging: boolean;
|
||||
projectId: string | null;
|
||||
projectName: string | null;
|
||||
allocationId: string | null;
|
||||
originalStartDate: Date | null;
|
||||
originalEndDate: Date | null;
|
||||
currentStartDate: Date | null;
|
||||
currentEndDate: Date | null;
|
||||
startMouseX: number;
|
||||
pointerDeltaX: number;
|
||||
originalLeft: number;
|
||||
blockWidth: number;
|
||||
daysDelta: number;
|
||||
};
|
||||
|
||||
describe("timelineProjectDrag", () => {
|
||||
it("creates project-bar drag state with default legacy metrics cleared", () => {
|
||||
expect(
|
||||
createProjectDragState<TestProjectDragState>({
|
||||
projectId: "project-1",
|
||||
projectName: "Alpha",
|
||||
startDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
endDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
startMouseX: 240,
|
||||
}),
|
||||
).toEqual({
|
||||
isDragging: true,
|
||||
projectId: "project-1",
|
||||
projectName: "Alpha",
|
||||
allocationId: null,
|
||||
originalStartDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
originalEndDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
currentStartDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
currentEndDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
startMouseX: 240,
|
||||
pointerDeltaX: 0,
|
||||
originalLeft: 0,
|
||||
blockWidth: 0,
|
||||
daysDelta: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves allocation metadata and block geometry for legacy allocation-block drags", () => {
|
||||
expect(
|
||||
createProjectDragState<TestProjectDragState>({
|
||||
projectId: "project-1",
|
||||
projectName: "Alpha",
|
||||
allocationId: "alloc-7",
|
||||
startDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
endDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
startMouseX: 320,
|
||||
originalLeft: 144,
|
||||
blockWidth: 96,
|
||||
}),
|
||||
).toMatchObject({
|
||||
allocationId: "alloc-7",
|
||||
originalLeft: 144,
|
||||
blockWidth: 96,
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses to build mutation input for no-op drags or incomplete state", () => {
|
||||
expect(
|
||||
buildProjectShiftMutationInput({
|
||||
projectId: "project-1",
|
||||
currentStartDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
currentEndDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
daysDelta: 0,
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
buildProjectShiftMutationInput({
|
||||
projectId: null,
|
||||
currentStartDate: new Date("2025-01-10T00:00:00.000Z"),
|
||||
currentEndDate: new Date("2025-01-20T00:00:00.000Z"),
|
||||
daysDelta: 2,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds mutation input only when a real drag produced complete dates", () => {
|
||||
expect(
|
||||
buildProjectShiftMutationInput({
|
||||
projectId: "project-1",
|
||||
currentStartDate: new Date("2025-01-12T00:00:00.000Z"),
|
||||
currentEndDate: new Date("2025-01-22T00:00:00.000Z"),
|
||||
daysDelta: 2,
|
||||
}),
|
||||
).toEqual({
|
||||
projectId: "project-1",
|
||||
newStartDate: new Date("2025-01-12T00:00:00.000Z"),
|
||||
newEndDate: new Date("2025-01-22T00:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
type ProjectDragStateLike = {
|
||||
isDragging: boolean;
|
||||
projectId: string | null;
|
||||
projectName: string | null;
|
||||
allocationId: string | null;
|
||||
originalStartDate: Date | null;
|
||||
originalEndDate: Date | null;
|
||||
currentStartDate: Date | null;
|
||||
currentEndDate: Date | null;
|
||||
startMouseX: number;
|
||||
pointerDeltaX: number;
|
||||
originalLeft: number;
|
||||
blockWidth: number;
|
||||
daysDelta: number;
|
||||
};
|
||||
|
||||
type CreateProjectDragStateInput = {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
allocationId?: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
startMouseX: number;
|
||||
originalLeft?: number;
|
||||
blockWidth?: number;
|
||||
};
|
||||
|
||||
export function createProjectDragState<TState extends ProjectDragStateLike>(
|
||||
input: CreateProjectDragStateInput,
|
||||
): TState {
|
||||
return {
|
||||
isDragging: true,
|
||||
projectId: input.projectId,
|
||||
projectName: input.projectName,
|
||||
allocationId: input.allocationId ?? null,
|
||||
originalStartDate: input.startDate,
|
||||
originalEndDate: input.endDate,
|
||||
currentStartDate: input.startDate,
|
||||
currentEndDate: input.endDate,
|
||||
startMouseX: input.startMouseX,
|
||||
pointerDeltaX: 0,
|
||||
originalLeft: input.originalLeft ?? 0,
|
||||
blockWidth: input.blockWidth ?? 0,
|
||||
daysDelta: 0,
|
||||
} as TState;
|
||||
}
|
||||
|
||||
export function buildProjectShiftMutationInput(
|
||||
drag: Pick<ProjectDragStateLike, "daysDelta" | "projectId" | "currentStartDate" | "currentEndDate">,
|
||||
): { projectId: string; newStartDate: Date; newEndDate: Date } | null {
|
||||
if (drag.daysDelta === 0 || !drag.projectId || !drag.currentStartDate || !drag.currentEndDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: drag.projectId,
|
||||
newStartDate: drag.currentStartDate,
|
||||
newEndDate: drag.currentEndDate,
|
||||
};
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
startAllocationMultiDrag,
|
||||
updateAllocationMultiDrag,
|
||||
} from "./timelineAllocationMultiDrag.js";
|
||||
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
|
||||
import {
|
||||
createMultiSelectState,
|
||||
finalizeMultiSelectDraft,
|
||||
@@ -448,17 +449,7 @@ export function useTimelineDrag({
|
||||
const finalDrag = dragStateRef.current;
|
||||
if (!finalDrag.isDragging) return null;
|
||||
|
||||
const mutationInput =
|
||||
finalDrag.daysDelta !== 0 &&
|
||||
finalDrag.projectId &&
|
||||
finalDrag.currentStartDate &&
|
||||
finalDrag.currentEndDate
|
||||
? {
|
||||
projectId: finalDrag.projectId,
|
||||
newStartDate: finalDrag.currentStartDate,
|
||||
newEndDate: finalDrag.currentEndDate,
|
||||
}
|
||||
: null;
|
||||
const mutationInput = buildProjectShiftMutationInput(finalDrag);
|
||||
|
||||
if (finalDrag.daysDelta !== 0) {
|
||||
preserveLivePreview(projectPreviewRef.current);
|
||||
@@ -543,21 +534,13 @@ export function useTimelineDrag({
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const state: DragState = {
|
||||
isDragging: true,
|
||||
const state = createProjectDragState<DragState>({
|
||||
projectId: opts.projectId,
|
||||
projectName: opts.projectName,
|
||||
allocationId: null,
|
||||
originalStartDate: opts.startDate,
|
||||
originalEndDate: opts.endDate,
|
||||
currentStartDate: opts.startDate,
|
||||
currentEndDate: opts.endDate,
|
||||
startDate: opts.startDate,
|
||||
endDate: opts.endDate,
|
||||
startMouseX: e.clientX,
|
||||
pointerDeltaX: 0,
|
||||
originalLeft: 0,
|
||||
blockWidth: 0,
|
||||
daysDelta: 0,
|
||||
};
|
||||
});
|
||||
dragStateRef.current = state;
|
||||
setDragState(state);
|
||||
|
||||
@@ -602,21 +585,16 @@ export function useTimelineDrag({
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const state: DragState = {
|
||||
isDragging: true,
|
||||
const state = createProjectDragState<DragState>({
|
||||
projectId: opts.projectId,
|
||||
projectName: opts.projectName,
|
||||
allocationId: opts.allocationId ?? null,
|
||||
originalStartDate: opts.startDate,
|
||||
originalEndDate: opts.endDate,
|
||||
currentStartDate: opts.startDate,
|
||||
currentEndDate: opts.endDate,
|
||||
startDate: opts.startDate,
|
||||
endDate: opts.endDate,
|
||||
startMouseX: e.clientX,
|
||||
pointerDeltaX: 0,
|
||||
originalLeft: opts.blockLeft,
|
||||
blockWidth: opts.blockWidth,
|
||||
daysDelta: 0,
|
||||
};
|
||||
});
|
||||
dragStateRef.current = state;
|
||||
setDragState(state);
|
||||
},
|
||||
|
||||
@@ -217,6 +217,21 @@ export const rules = [
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
{
|
||||
file: "apps/web/src/hooks/timelineProjectDrag.ts",
|
||||
maxLines: 80,
|
||||
required: [
|
||||
{
|
||||
pattern: /\bexport function createProjectDragState\b/,
|
||||
message: "timeline project drag helpers must keep drag-state bootstrap centralized",
|
||||
},
|
||||
{
|
||||
pattern: /\bexport function buildProjectShiftMutationInput\b/,
|
||||
message: "timeline project drag helpers must keep no-op project-shift mutation gating centralized",
|
||||
},
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
{
|
||||
file: "apps/web/src/hooks/useTimelineDrag.ts",
|
||||
required: [
|
||||
@@ -248,6 +263,10 @@ export const rules = [
|
||||
pattern: /from "\.\/timelineAllocationMultiDrag\.js"/,
|
||||
message: "timeline drag must keep allocation multi-drag rules delegated to the extracted helper module",
|
||||
},
|
||||
{
|
||||
pattern: /from "\.\/timelineProjectDrag\.js"/,
|
||||
message: "timeline drag must keep project drag bootstrap and mutation gating delegated to the extracted helper module",
|
||||
},
|
||||
],
|
||||
forbidden: [
|
||||
{
|
||||
@@ -266,6 +285,10 @@ export const rules = [
|
||||
pattern: /\bfunction (?:isAllocationMultiSelected|startAllocationMultiDrag|updateAllocationMultiDrag|finalizeAllocationMultiDrag)\b/,
|
||||
message: "timeline drag must not re-inline extracted allocation multi-drag helper implementations",
|
||||
},
|
||||
{
|
||||
pattern: /\bfunction (?:createProjectDragState|buildProjectShiftMutationInput)\b/,
|
||||
message: "timeline drag must not re-inline extracted project drag helper implementations",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ describe("architecture guardrails", () => {
|
||||
const optimisticRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineOptimisticAllocations.ts");
|
||||
const allocationFinalizeRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationFinalize.ts");
|
||||
const allocationMultiDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationMultiDrag.ts");
|
||||
const projectDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineProjectDrag.ts");
|
||||
|
||||
assert.ok(dragRule);
|
||||
assert.ok(livePreviewRule);
|
||||
@@ -86,6 +87,7 @@ describe("architecture guardrails", () => {
|
||||
assert.ok(optimisticRule);
|
||||
assert.ok(allocationFinalizeRule);
|
||||
assert.ok(allocationMultiDragRule);
|
||||
assert.ok(projectDragRule);
|
||||
|
||||
assert.deepEqual(evaluateRule(dragRule, "function clearLivePreview() {}\n"), [
|
||||
"apps/web/src/hooks/useTimelineDrag.ts: missing guardrail anchor: timeline drag must keep live preview behavior delegated to the extracted helper module",
|
||||
@@ -95,6 +97,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 allocation drag completion 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 project drag bootstrap and mutation gating 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",
|
||||
]);
|
||||
|
||||
@@ -129,5 +132,9 @@ describe("architecture guardrails", () => {
|
||||
"apps/web/src/hooks/timelineAllocationMultiDrag.ts: missing guardrail anchor: timeline allocation multi-drag helpers must keep same-day delta suppression centralized",
|
||||
"apps/web/src/hooks/timelineAllocationMultiDrag.ts: missing guardrail anchor: timeline allocation multi-drag helpers must keep reset-on-release behavior centralized",
|
||||
]);
|
||||
|
||||
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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user