refactor(web): extract allocation drag bootstrap

This commit is contained in:
2026-04-01 10:10:06 +02:00
parent 892a9c5ccf
commit 203bb8751d
5 changed files with 162 additions and 13 deletions
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import { createAllocationDragState, type AllocationDragStateLike } from "./timelineAllocationDragState.js";
describe("timelineAllocationDragState", () => {
it("defaults scope and mutation ids for whole-allocation drags", () => {
expect(
createAllocationDragState<AllocationDragStateLike<"move", "allocation" | "segment">, "move", "allocation" | "segment">({
mode: "move",
allocationId: "alloc-1",
projectId: "project-1",
projectName: "Alpha",
resourceId: null,
startDate: new Date("2025-01-01T00:00:00.000Z"),
endDate: new Date("2025-01-05T00:00:00.000Z"),
startMouseX: 320,
}),
).toEqual({
isActive: true,
mode: "move",
scope: "allocation",
allocationId: "alloc-1",
mutationAllocationId: "alloc-1",
projectId: "project-1",
projectName: "Alpha",
resourceId: null,
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"),
startMouseX: 320,
pointerDeltaX: 0,
daysDelta: 0,
});
});
it("preserves explicit mutation ids and segment boundaries for fragment drags", () => {
expect(
createAllocationDragState<
AllocationDragStateLike<"resize-end", "allocation" | "segment">,
"resize-end",
"allocation" | "segment"
>({
mode: "resize-end",
scope: "segment",
allocationId: "alloc-1",
mutationAllocationId: "alloc-fragment-2",
projectId: "project-1",
projectName: "Alpha",
resourceId: "resource-4",
startDate: new Date("2025-01-03T00:00:00.000Z"),
endDate: new Date("2025-01-04T00:00:00.000Z"),
allocationStartDate: new Date("2025-01-01T00:00:00.000Z"),
allocationEndDate: new Date("2025-01-07T00:00:00.000Z"),
startMouseX: 144,
}),
).toMatchObject({
mode: "resize-end",
scope: "segment",
mutationAllocationId: "alloc-fragment-2",
resourceId: "resource-4",
allocationStartDate: new Date("2025-01-01T00:00:00.000Z"),
allocationEndDate: new Date("2025-01-07T00:00:00.000Z"),
});
});
});
@@ -0,0 +1,60 @@
export type AllocationDragStateLike<TMode extends string = string, TScope extends string = string> = {
isActive: boolean;
mode: TMode;
scope: TScope;
allocationId: string | null;
mutationAllocationId: string | null;
projectId: string | null;
projectName: string | null;
resourceId: string | null;
allocationStartDate: Date | null;
allocationEndDate: Date | null;
originalStartDate: Date | null;
originalEndDate: Date | null;
currentStartDate: Date | null;
currentEndDate: Date | null;
startMouseX: number;
pointerDeltaX: number;
daysDelta: number;
};
type CreateAllocationDragStateInput<TMode extends string, TScope extends string> = {
mode: TMode;
scope?: TScope | undefined;
allocationId: string;
mutationAllocationId?: string | undefined;
projectId: string;
projectName: string;
resourceId: string | null;
startDate: Date;
endDate: Date;
allocationStartDate?: Date | undefined;
allocationEndDate?: Date | undefined;
startMouseX: number;
};
export function createAllocationDragState<
TState extends AllocationDragStateLike<TMode, TScope>,
TMode extends string,
TScope extends string,
>(input: CreateAllocationDragStateInput<TMode, TScope>): TState {
return {
isActive: true,
mode: input.mode,
scope: input.scope ?? ("allocation" as TScope),
allocationId: input.allocationId,
mutationAllocationId: input.mutationAllocationId ?? input.allocationId,
projectId: input.projectId,
projectName: input.projectName,
resourceId: input.resourceId,
allocationStartDate: input.allocationStartDate ?? input.startDate,
allocationEndDate: input.allocationEndDate ?? input.endDate,
originalStartDate: input.startDate,
originalEndDate: input.endDate,
currentStartDate: input.startDate,
currentEndDate: input.endDate,
startMouseX: input.startMouseX,
pointerDeltaX: 0,
daysDelta: 0,
} as TState;
}
+9 -13
View File
@@ -23,6 +23,7 @@ import {
startAllocationMultiDrag, startAllocationMultiDrag,
updateAllocationMultiDrag, updateAllocationMultiDrag,
} from "./timelineAllocationMultiDrag.js"; } from "./timelineAllocationMultiDrag.js";
import { createAllocationDragState } from "./timelineAllocationDragState.js";
import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js"; import { buildProjectShiftMutationInput, createProjectDragState } from "./timelineProjectDrag.js";
import { import {
createMultiSelectState, createMultiSelectState,
@@ -682,25 +683,20 @@ export function useTimelineDrag({
// ── Single allocation drag ──────────────────────────────────────────── // ── Single allocation drag ────────────────────────────────────────────
const initial: AllocDragState = { const initial = createAllocationDragState<AllocDragState, AllocDragMode, AllocDragScope>({
isActive: true,
mode: opts.mode, mode: opts.mode,
scope: opts.scope ?? "allocation", scope: opts.scope,
allocationId: opts.allocationId, allocationId: opts.allocationId,
mutationAllocationId: opts.mutationAllocationId ?? opts.allocationId, mutationAllocationId: opts.mutationAllocationId,
projectId: opts.projectId, projectId: opts.projectId,
projectName: opts.projectName, projectName: opts.projectName,
resourceId: opts.resourceId, resourceId: opts.resourceId,
allocationStartDate: opts.allocationStartDate ?? opts.startDate, allocationStartDate: opts.allocationStartDate,
allocationEndDate: opts.allocationEndDate ?? opts.endDate, allocationEndDate: opts.allocationEndDate,
originalStartDate: opts.startDate, startDate: opts.startDate,
originalEndDate: opts.endDate, endDate: opts.endDate,
currentStartDate: opts.startDate,
currentEndDate: opts.endDate,
startMouseX: e.clientX, startMouseX: e.clientX,
pointerDeltaX: 0, });
daysDelta: 0,
};
allocDragRef.current = initial; allocDragRef.current = initial;
setAllocDragState(initial); setAllocDragState(initial);
setAllocationPreviewTarget(e.currentTarget, opts.mode); setAllocationPreviewTarget(e.currentTarget, opts.mode);
+19
View File
@@ -217,6 +217,17 @@ export const rules = [
], ],
forbidden: [], forbidden: [],
}, },
{
file: "apps/web/src/hooks/timelineAllocationDragState.ts",
maxLines: 80,
required: [
{
pattern: /\bexport function createAllocationDragState\b/,
message: "timeline allocation drag state helpers must keep drag bootstrap centralized",
},
],
forbidden: [],
},
{ {
file: "apps/web/src/hooks/timelineProjectDrag.ts", file: "apps/web/src/hooks/timelineProjectDrag.ts",
maxLines: 80, maxLines: 80,
@@ -263,6 +274,10 @@ export const rules = [
pattern: /from "\.\/timelineAllocationMultiDrag\.js"/, pattern: /from "\.\/timelineAllocationMultiDrag\.js"/,
message: "timeline drag must keep allocation multi-drag rules delegated to the extracted helper module", message: "timeline drag must keep allocation multi-drag rules delegated to the extracted helper module",
}, },
{
pattern: /from "\.\/timelineAllocationDragState\.js"/,
message: "timeline drag must keep allocation drag bootstrap 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",
@@ -285,6 +300,10 @@ export const rules = [
pattern: /\bfunction (?:isAllocationMultiSelected|startAllocationMultiDrag|updateAllocationMultiDrag|finalizeAllocationMultiDrag)\b/, pattern: /\bfunction (?:isAllocationMultiSelected|startAllocationMultiDrag|updateAllocationMultiDrag|finalizeAllocationMultiDrag)\b/,
message: "timeline drag must not re-inline extracted allocation multi-drag helper implementations", message: "timeline drag must not re-inline extracted allocation multi-drag helper implementations",
}, },
{
pattern: /\bfunction createAllocationDragState\b/,
message: "timeline drag must not re-inline extracted allocation drag bootstrap helpers",
},
{ {
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",
@@ -77,6 +77,7 @@ describe("architecture guardrails", () => {
const optimisticRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineOptimisticAllocations.ts"); 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 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 allocationMultiDragRule = rules.find((rule) => rule.file === "apps/web/src/hooks/timelineAllocationMultiDrag.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");
assert.ok(dragRule); assert.ok(dragRule);
@@ -87,6 +88,7 @@ describe("architecture guardrails", () => {
assert.ok(optimisticRule); assert.ok(optimisticRule);
assert.ok(allocationFinalizeRule); assert.ok(allocationFinalizeRule);
assert.ok(allocationMultiDragRule); assert.ok(allocationMultiDragRule);
assert.ok(allocationDragStateRule);
assert.ok(projectDragRule); assert.ok(projectDragRule);
assert.deepEqual(evaluateRule(dragRule, "function clearLivePreview() {}\n"), [ assert.deepEqual(evaluateRule(dragRule, "function clearLivePreview() {}\n"), [
@@ -97,6 +99,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 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 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: 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",
]); ]);
@@ -133,6 +136,10 @@ describe("architecture guardrails", () => {
"apps/web/src/hooks/timelineAllocationMultiDrag.ts: missing guardrail anchor: timeline allocation multi-drag helpers must keep reset-on-release behavior 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(allocationDragStateRule, ""), [
"apps/web/src/hooks/timelineAllocationDragState.ts: missing guardrail anchor: timeline allocation drag state helpers must keep drag bootstrap centralized",
]);
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",
]); ]);