feat: timeline UI overhaul with project/resource panel redesign, quick filters, and API improvements
Redesigned timeline project and resource panels with expanded detail views, added quick filter toolbar, improved drag handling, and enhanced vacation/entitlement router logic. Includes e2e test updates and minor API fixes. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -154,7 +154,11 @@ export function useTimelineDrag({
|
||||
cellWidthRef.current = cellWidth;
|
||||
|
||||
// Touch disambiguation: track initial touch position to distinguish horizontal drag from vertical scroll
|
||||
const touchStartRef = useRef<{ x: number; y: number; decided: boolean }>({ x: 0, y: 0, decided: false });
|
||||
const touchStartRef = useRef<{ x: number; y: number; decided: boolean }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
decided: false,
|
||||
});
|
||||
|
||||
const onBlockClickRef = useRef(onBlockClick);
|
||||
onBlockClickRef.current = onBlockClick;
|
||||
@@ -190,7 +194,11 @@ export function useTimelineDrag({
|
||||
void utils.project.list.invalidate();
|
||||
onShiftApplied?.(data.project.id);
|
||||
},
|
||||
}) as { isPending: boolean; mutate: (...args: unknown[]) => void; mutateAsync: (...args: unknown[]) => Promise<unknown> };
|
||||
}) as {
|
||||
isPending: boolean;
|
||||
mutate: (...args: unknown[]) => void;
|
||||
mutateAsync: (...args: unknown[]) => Promise<unknown>;
|
||||
};
|
||||
|
||||
const pendingSnapshotRef = useRef<AllocationMovedSnapshot | null>(null);
|
||||
|
||||
@@ -211,12 +219,16 @@ export function useTimelineDrag({
|
||||
// ── Project-bar drag (shifts all allocations) ──────────────────────────────
|
||||
|
||||
const onProjectBarMouseDown = useCallback(
|
||||
(e: React.MouseEvent, opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) => {
|
||||
(
|
||||
e: React.MouseEvent,
|
||||
opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const state: DragState = {
|
||||
@@ -241,15 +253,19 @@ export function useTimelineDrag({
|
||||
|
||||
// Legacy — kept for backward compat (triggers project shift from allocation block)
|
||||
const onBlockMouseDown = useCallback(
|
||||
(e: React.MouseEvent, opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
allocationId?: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
blockLeft: number;
|
||||
blockWidth: number;
|
||||
}) => {
|
||||
(
|
||||
e: React.MouseEvent,
|
||||
opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
allocationId?: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
blockLeft: number;
|
||||
blockWidth: number;
|
||||
},
|
||||
) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const state: DragState = {
|
||||
@@ -279,16 +295,20 @@ export function useTimelineDrag({
|
||||
// moving quickly or scrolling into the sticky header area).
|
||||
|
||||
const onAllocMouseDown = useCallback(
|
||||
(e: React.MouseEvent, opts: {
|
||||
mode: AllocDragMode;
|
||||
allocationId: string;
|
||||
mutationAllocationId?: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
resourceId: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) => {
|
||||
(
|
||||
e: React.MouseEvent,
|
||||
opts: {
|
||||
mode: AllocDragMode;
|
||||
allocationId: string;
|
||||
mutationAllocationId?: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
resourceId: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -389,12 +409,16 @@ export function useTimelineDrag({
|
||||
// ── Range-select ────────────────────────────────────────────────────────────
|
||||
|
||||
const onRowMouseDown = useCallback(
|
||||
(e: React.MouseEvent, opts: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
suggestedProjectId?: string;
|
||||
}) => {
|
||||
(
|
||||
e: React.MouseEvent,
|
||||
opts: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
suggestedProjectId?: string;
|
||||
},
|
||||
) => {
|
||||
if (dragStateRef.current.isDragging || allocDragRef.current.isActive) return;
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
const state: RangeState = {
|
||||
isSelecting: true,
|
||||
@@ -424,7 +448,12 @@ export function useTimelineDrag({
|
||||
newStart.setDate(newStart.getDate() + daysDelta);
|
||||
const newEnd = new Date(drag.originalEndDate);
|
||||
newEnd.setDate(newEnd.getDate() + daysDelta);
|
||||
const updated: DragState = { ...drag, currentStartDate: newStart, currentEndDate: newEnd, daysDelta };
|
||||
const updated: DragState = {
|
||||
...drag,
|
||||
currentStartDate: newStart,
|
||||
currentEndDate: newEnd,
|
||||
daysDelta,
|
||||
};
|
||||
dragStateRef.current = updated;
|
||||
setDragState(updated);
|
||||
}
|
||||
@@ -488,9 +517,7 @@ export function useTimelineDrag({
|
||||
if (range.isSelecting && range.resourceId && range.startDate) {
|
||||
const endDate = range.currentDate ?? range.startDate;
|
||||
const [startDate, finalEnd] =
|
||||
range.startDate <= endDate
|
||||
? [range.startDate, endDate]
|
||||
: [endDate, range.startDate];
|
||||
range.startDate <= endDate ? [range.startDate, endDate] : [endDate, range.startDate];
|
||||
|
||||
onRangeSelected?.({
|
||||
resourceId: range.resourceId,
|
||||
@@ -529,16 +556,23 @@ export function useTimelineDrag({
|
||||
}
|
||||
|
||||
const onProjectBarTouchStart = useCallback(
|
||||
(e: React.TouchEvent, opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) => {
|
||||
(
|
||||
e: React.TouchEvent,
|
||||
opts: {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: true };
|
||||
onProjectBarMouseDown(
|
||||
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
|
||||
{
|
||||
clientX: toClientX(e),
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
} as unknown as React.MouseEvent,
|
||||
opts,
|
||||
);
|
||||
},
|
||||
@@ -546,20 +580,27 @@ export function useTimelineDrag({
|
||||
);
|
||||
|
||||
const onAllocTouchStart = useCallback(
|
||||
(e: React.TouchEvent, opts: {
|
||||
mode: AllocDragMode;
|
||||
allocationId: string;
|
||||
mutationAllocationId?: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
resourceId: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) => {
|
||||
(
|
||||
e: React.TouchEvent,
|
||||
opts: {
|
||||
mode: AllocDragMode;
|
||||
allocationId: string;
|
||||
mutationAllocationId?: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
resourceId: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: true };
|
||||
onAllocMouseDown(
|
||||
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
|
||||
{
|
||||
clientX: toClientX(e),
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
} as unknown as React.MouseEvent,
|
||||
opts,
|
||||
);
|
||||
},
|
||||
@@ -567,15 +608,22 @@ export function useTimelineDrag({
|
||||
);
|
||||
|
||||
const onRowTouchStart = useCallback(
|
||||
(e: React.TouchEvent, opts: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
suggestedProjectId?: string;
|
||||
}) => {
|
||||
(
|
||||
e: React.TouchEvent,
|
||||
opts: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
suggestedProjectId?: string;
|
||||
},
|
||||
) => {
|
||||
e.preventDefault();
|
||||
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: false };
|
||||
onRowMouseDown(
|
||||
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
|
||||
{
|
||||
clientX: toClientX(e),
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
} as unknown as React.MouseEvent,
|
||||
opts,
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user