refactor(web): extract allocation drag bootstrap
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user