feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -9,6 +9,7 @@ import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
||||
import { useTimelineLayout } from "~/hooks/useTimelineLayout.js";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js";
|
||||
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
|
||||
import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
|
||||
import { AllocationPopover } from "./AllocationPopover.js";
|
||||
import { DemandPopover } from "./DemandPopover.js";
|
||||
@@ -33,6 +34,7 @@ import { TimelineResourcePanel } from "./TimelineResourcePanel.js";
|
||||
import { TimelineProjectPanel, type OpenDemandAssignment } from "./TimelineProjectPanel.js";
|
||||
import { ProjectColorLegend } from "./ProjectColorLegend.js";
|
||||
import { useMultiSelectIntersection } from "~/hooks/useMultiSelectIntersection.js";
|
||||
import type { TimelineVisualOverrides } from "./allocationVisualState.js";
|
||||
|
||||
// ─── Entry point ────────────────────────────────────────────────────────────
|
||||
// Two-layer mount: the outer shell creates drag state + project context,
|
||||
@@ -56,8 +58,10 @@ export function TimelineView() {
|
||||
const [popover, setPopover] = useState<{
|
||||
allocationId: string;
|
||||
projectId: string;
|
||||
allocation?: TimelineAssignmentEntry | null;
|
||||
x: number;
|
||||
y: number;
|
||||
contextDate?: Date;
|
||||
} | null>(null);
|
||||
const [newAllocPopover, setNewAllocPopover] = useState<{
|
||||
resourceId: string;
|
||||
@@ -88,6 +92,8 @@ export function TimelineView() {
|
||||
rangeState,
|
||||
multiSelectState,
|
||||
setMultiSelectState,
|
||||
optimisticAllocations,
|
||||
reconcileOptimisticAllocations,
|
||||
shiftPreview,
|
||||
isPreviewLoading,
|
||||
isApplying,
|
||||
@@ -106,7 +112,7 @@ export function TimelineView() {
|
||||
onCanvasTouchMove,
|
||||
onCanvasTouchEnd,
|
||||
} = useTimelineDrag({
|
||||
cellWidth: cellWidthRef.current,
|
||||
cellWidthRef,
|
||||
onBlockClick: (info) => {
|
||||
setPopover({
|
||||
allocationId: info.allocationId,
|
||||
@@ -170,6 +176,8 @@ export function TimelineView() {
|
||||
rangeState={rangeState}
|
||||
multiSelectState={multiSelectState}
|
||||
setMultiSelectState={setMultiSelectState}
|
||||
optimisticAllocations={optimisticAllocations}
|
||||
reconcileOptimisticAllocations={reconcileOptimisticAllocations}
|
||||
onCanvasRightMouseDown={onCanvasRightMouseDown}
|
||||
clearMultiSelect={clearMultiSelect}
|
||||
shiftPreview={shiftPreview}
|
||||
@@ -214,6 +222,8 @@ function TimelineViewContent({
|
||||
rangeState,
|
||||
multiSelectState,
|
||||
setMultiSelectState,
|
||||
optimisticAllocations,
|
||||
reconcileOptimisticAllocations,
|
||||
onCanvasRightMouseDown,
|
||||
clearMultiSelect,
|
||||
shiftPreview,
|
||||
@@ -251,6 +261,8 @@ function TimelineViewContent({
|
||||
rangeState: ReturnType<typeof useTimelineDrag>["rangeState"];
|
||||
multiSelectState: ReturnType<typeof useTimelineDrag>["multiSelectState"];
|
||||
setMultiSelectState: ReturnType<typeof useTimelineDrag>["setMultiSelectState"];
|
||||
optimisticAllocations: TimelineVisualOverrides;
|
||||
reconcileOptimisticAllocations: ReturnType<typeof useTimelineDrag>["reconcileOptimisticAllocations"];
|
||||
onCanvasRightMouseDown: ReturnType<typeof useTimelineDrag>["onCanvasRightMouseDown"];
|
||||
clearMultiSelect: ReturnType<typeof useTimelineDrag>["clearMultiSelect"];
|
||||
shiftPreview: ReturnType<typeof useTimelineDrag>["shiftPreview"];
|
||||
@@ -269,7 +281,14 @@ function TimelineViewContent({
|
||||
onCanvasTouchMove: ReturnType<typeof useTimelineDrag>["onCanvasTouchMove"];
|
||||
onCanvasTouchEnd: ReturnType<typeof useTimelineDrag>["onCanvasTouchEnd"];
|
||||
contextResourceIds: string[];
|
||||
popover: { allocationId: string; projectId: string; x: number; y: number } | null;
|
||||
popover: {
|
||||
allocationId: string;
|
||||
projectId: string;
|
||||
allocation?: TimelineAssignmentEntry | null;
|
||||
x: number;
|
||||
y: number;
|
||||
contextDate?: Date;
|
||||
} | null;
|
||||
setPopover: React.Dispatch<React.SetStateAction<typeof popover>>;
|
||||
newAllocPopover: {
|
||||
resourceId: string;
|
||||
@@ -300,6 +319,8 @@ function TimelineViewContent({
|
||||
viewStart,
|
||||
viewEnd,
|
||||
viewDays,
|
||||
visibleAssignments,
|
||||
visibleDemands,
|
||||
setViewStart,
|
||||
setViewDays,
|
||||
filters,
|
||||
@@ -348,6 +369,23 @@ function TimelineViewContent({
|
||||
const hasActivePointerOverlay =
|
||||
dragState.isDragging || allocDragState.isActive || rangeState.isSelecting || multiSelectState.isMultiDragging;
|
||||
|
||||
useEffect(() => {
|
||||
if (optimisticAllocations.size === 0) return;
|
||||
reconcileOptimisticAllocations([...visibleAssignments, ...visibleDemands]);
|
||||
}, [
|
||||
optimisticAllocations,
|
||||
reconcileOptimisticAllocations,
|
||||
visibleAssignments,
|
||||
visibleDemands,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasActivePointerOverlay) return;
|
||||
setPopover(null);
|
||||
setDemandPopover(null);
|
||||
setResourceHover(null);
|
||||
}, [hasActivePointerOverlay]);
|
||||
|
||||
// ─── Keep selection overlay visible while popover is open ───────────────────
|
||||
const effectiveRangeState: typeof rangeState = rangeState.isSelecting
|
||||
? rangeState
|
||||
@@ -386,10 +424,12 @@ function TimelineViewContent({
|
||||
info: {
|
||||
allocationId: string;
|
||||
projectId: string;
|
||||
contextDate?: Date;
|
||||
},
|
||||
anchorX: number,
|
||||
anchorY: number,
|
||||
) {
|
||||
if (hasActivePointerOverlay) return;
|
||||
// Check if this is a demand (not an assignment) — route to DemandPopover
|
||||
const demands = openDemandsByProject.get(info.projectId);
|
||||
const demand = demands?.find((d) => d.id === info.allocationId);
|
||||
@@ -397,11 +437,19 @@ function TimelineViewContent({
|
||||
setDemandPopover({ demand, x: anchorX, y: anchorY });
|
||||
return;
|
||||
}
|
||||
const allocation = visibleAssignments.find((entry) => (
|
||||
entry.id === info.allocationId
|
||||
|| entry.entityId === info.allocationId
|
||||
|| entry.sourceAllocationId === info.allocationId
|
||||
|| getPlanningEntryMutationId(entry) === info.allocationId
|
||||
)) ?? null;
|
||||
setPopover({
|
||||
allocationId: info.allocationId,
|
||||
projectId: info.projectId,
|
||||
allocation,
|
||||
x: anchorX,
|
||||
y: anchorY,
|
||||
...(info.contextDate ? { contextDate: info.contextDate } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -505,12 +553,22 @@ function TimelineViewContent({
|
||||
|
||||
// ─── Resource hover card — event delegation on label columns ──────────────
|
||||
useEffect(() => {
|
||||
if (hasActivePointerOverlay) {
|
||||
if (resourceHoverTimerRef.current) {
|
||||
clearTimeout(resourceHoverTimerRef.current);
|
||||
resourceHoverTimerRef.current = null;
|
||||
}
|
||||
setResourceHover(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const HOVER_DELAY = 400;
|
||||
|
||||
function onMouseOver(e: MouseEvent) {
|
||||
if (hasActivePointerOverlay) return;
|
||||
const target = (e.target as HTMLElement).closest<HTMLElement>("[data-resource-hover-id]");
|
||||
if (!target) return;
|
||||
const rid = target.dataset.resourceHoverId;
|
||||
@@ -532,6 +590,7 @@ function TimelineViewContent({
|
||||
}
|
||||
|
||||
function onMouseOut(e: MouseEvent) {
|
||||
if (hasActivePointerOverlay) return;
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
// Don't close if moving into another resource-hover target or the hover card itself
|
||||
if (related?.closest?.("[data-resource-hover-id]") || related?.closest?.("[data-resource-hover-card]")) return;
|
||||
@@ -557,7 +616,7 @@ function TimelineViewContent({
|
||||
resourceHoverTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [resourceHover?.resourceId, isInitialLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [resourceHover?.resourceId, isInitialLoading, hasActivePointerOverlay]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// ─── Lazy-extend date range on scroll ─────────────────────────────────────
|
||||
function handleContainerScroll() {
|
||||
@@ -682,6 +741,8 @@ function TimelineViewContent({
|
||||
onRowTouchStart={isSelfServiceTimeline ? () => undefined : onRowTouchStart}
|
||||
onAllocationContextMenu={isSelfServiceTimeline ? () => undefined : openAllocationPopoverAt}
|
||||
multiSelectState={multiSelectState}
|
||||
optimisticAllocations={optimisticAllocations}
|
||||
suppressHoverInteractions={hasActivePointerOverlay}
|
||||
CELL_WIDTH={CELL_WIDTH}
|
||||
dates={dates}
|
||||
totalCanvasWidth={totalCanvasWidth}
|
||||
@@ -707,9 +768,12 @@ function TimelineViewContent({
|
||||
onRowTouchStart={isSelfServiceTimeline ? () => undefined : onRowTouchStart}
|
||||
onOpenPanel={isSelfServiceTimeline ? () => undefined : setOpenPanelProjectId}
|
||||
onOpenDemandClick={isSelfServiceTimeline ? () => undefined : (demand, anchorX, anchorY) => {
|
||||
if (hasActivePointerOverlay) return;
|
||||
setDemandPopover({ demand, x: anchorX, y: anchorY });
|
||||
}}
|
||||
onAllocationContextMenu={isSelfServiceTimeline ? () => undefined : openAllocationPopoverAt}
|
||||
optimisticAllocations={optimisticAllocations}
|
||||
suppressHoverInteractions={hasActivePointerOverlay}
|
||||
CELL_WIDTH={CELL_WIDTH}
|
||||
dates={dates}
|
||||
totalCanvasWidth={totalCanvasWidth}
|
||||
@@ -827,7 +891,7 @@ function TimelineViewContent({
|
||||
)}
|
||||
|
||||
{/* Allocation / Demand popover (click path) */}
|
||||
{!isSelfServiceTimeline && popover && (() => {
|
||||
{!isSelfServiceTimeline && !hasActivePointerOverlay && popover && (() => {
|
||||
// Check if clicked allocation is actually a demand
|
||||
const clickedDemand = openDemandsByProject.get(popover.projectId)?.find((d) => d.id === popover.allocationId);
|
||||
if (clickedDemand) {
|
||||
@@ -863,6 +927,7 @@ function TimelineViewContent({
|
||||
<AllocationPopover
|
||||
allocationId={popover.allocationId}
|
||||
projectId={popover.projectId}
|
||||
initialAllocation={popover.allocation ?? null}
|
||||
onClose={() => setPopover(null)}
|
||||
onOpenPanel={(pid) => {
|
||||
setPopover(null);
|
||||
@@ -870,12 +935,13 @@ function TimelineViewContent({
|
||||
}}
|
||||
anchorX={popover.x}
|
||||
anchorY={popover.y}
|
||||
{...(popover.contextDate ? { contextDate: popover.contextDate } : {})}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Demand popover */}
|
||||
{!isSelfServiceTimeline && demandPopover && (
|
||||
{!isSelfServiceTimeline && !hasActivePointerOverlay && demandPopover && (
|
||||
<DemandPopover
|
||||
demand={demandPopover.demand}
|
||||
onClose={() => setDemandPopover(null)}
|
||||
@@ -962,7 +1028,7 @@ function TimelineViewContent({
|
||||
)}
|
||||
|
||||
{/* Resource hover card */}
|
||||
{resourceHover && (
|
||||
{!hasActivePointerOverlay && resourceHover && (
|
||||
<ResourceHoverCard
|
||||
resourceId={resourceHover.resourceId}
|
||||
anchorEl={resourceHover.anchorEl}
|
||||
|
||||
Reference in New Issue
Block a user