refactor(web): split TimelineContext into data, view, and display contexts
Reduces unnecessary re-renders by separating the monolithic 20+ property context into TimelineDataContext, TimelineViewContext, and TimelineDisplayContext. Panel components now subscribe only to the slices they need. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useTimelineContext } from "./TimelineContext.js";
|
||||
import { useTimelineData, useTimelineView } from "./TimelineContext.js";
|
||||
import { getProjectColor } from "~/lib/project-colors.js";
|
||||
import { FadeIn } from "~/components/ui/FadeIn.js";
|
||||
|
||||
function ProjectColorLegendInner() {
|
||||
const { visibleAssignments, viewMode, projectGroups } = useTimelineContext();
|
||||
const { visibleAssignments, projectGroups } = useTimelineData();
|
||||
const { viewMode } = useTimelineView();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
// Collect unique visible projects with their colors
|
||||
@@ -76,7 +77,13 @@ function ProjectColorLegendInner() {
|
||||
)}
|
||||
aria-label="Dismiss color legend"
|
||||
>
|
||||
<svg className="w-3 h-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M2 2l8 8M10 2l-8 8" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -176,10 +176,9 @@ export type HolidayOverlayEntry = {
|
||||
metroCityName?: string | null;
|
||||
};
|
||||
|
||||
// ─── Context shape ──────────────────────────────────────────────────────────
|
||||
// ─── Context shapes ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface TimelineContextValue {
|
||||
// ─ Data
|
||||
export interface TimelineDataContextValue {
|
||||
assignments: TimelineAssignmentEntry[];
|
||||
demands: TimelineDemandEntry[];
|
||||
visibleAssignments: TimelineAssignmentEntry[];
|
||||
@@ -190,8 +189,13 @@ export interface TimelineContextValue {
|
||||
allocsByResource: Map<string, TimelineAssignmentEntry[]>;
|
||||
projectGroups: ProjectGroup[];
|
||||
openDemandsByProject: Map<string, TimelineDemandEntry[]>;
|
||||
isLoading: boolean;
|
||||
isInitialLoading: boolean;
|
||||
isEntriesError: boolean;
|
||||
totalAllocCount: number;
|
||||
}
|
||||
|
||||
// ─ View state
|
||||
export interface TimelineViewContextValue {
|
||||
viewStart: Date;
|
||||
viewEnd: Date;
|
||||
viewDays: number;
|
||||
@@ -204,32 +208,46 @@ export interface TimelineContextValue {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: React.Dispatch<React.SetStateAction<ViewMode>>;
|
||||
today: Date;
|
||||
activeFilterCount: number;
|
||||
}
|
||||
|
||||
// ─ Display preferences
|
||||
export interface TimelineDisplayContextValue {
|
||||
displayMode: TimelineDisplayMode;
|
||||
heatmapScheme: HeatmapColorScheme;
|
||||
blinkOverbookedDays: boolean;
|
||||
|
||||
// ─ Loading
|
||||
isLoading: boolean;
|
||||
isInitialLoading: boolean;
|
||||
isEntriesError: boolean;
|
||||
totalAllocCount: number;
|
||||
activeFilterCount: number;
|
||||
|
||||
// ─ SSE is initialized by the provider (no value exposed)
|
||||
}
|
||||
|
||||
const TimelineContext = createContext<TimelineContextValue | null>(null);
|
||||
export type TimelineContextValue = TimelineDataContextValue &
|
||||
TimelineViewContextValue &
|
||||
TimelineDisplayContextValue;
|
||||
|
||||
export function useTimelineContext(): TimelineContextValue {
|
||||
const ctx = useContext(TimelineContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useTimelineContext must be used within a <TimelineProvider>");
|
||||
}
|
||||
const TimelineDataContext = createContext<TimelineDataContextValue | null>(null);
|
||||
const TimelineViewContext = createContext<TimelineViewContextValue | null>(null);
|
||||
const TimelineDisplayContext = createContext<TimelineDisplayContextValue | null>(null);
|
||||
|
||||
export function useTimelineData(): TimelineDataContextValue {
|
||||
const ctx = useContext(TimelineDataContext);
|
||||
if (!ctx) throw new Error("useTimelineData must be used within a <TimelineProvider>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useTimelineView(): TimelineViewContextValue {
|
||||
const ctx = useContext(TimelineViewContext);
|
||||
if (!ctx) throw new Error("useTimelineView must be used within a <TimelineProvider>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useTimelineDisplay(): TimelineDisplayContextValue {
|
||||
const ctx = useContext(TimelineDisplayContext);
|
||||
if (!ctx) throw new Error("useTimelineDisplay must be used within a <TimelineProvider>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Combined hook — use the specific hooks above to avoid unnecessary re-renders. */
|
||||
export function useTimelineContext(): TimelineContextValue {
|
||||
return { ...useTimelineData(), ...useTimelineView(), ...useTimelineDisplay() };
|
||||
}
|
||||
|
||||
// ─── Provider ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface TimelineProviderProps {
|
||||
@@ -752,7 +770,7 @@ export function TimelineProvider({
|
||||
filters.projectIds.length +
|
||||
filters.countryCodes.length;
|
||||
|
||||
const value = useMemo<TimelineContextValue>(
|
||||
const dataValue = useMemo<TimelineDataContextValue>(
|
||||
() => ({
|
||||
assignments,
|
||||
demands,
|
||||
@@ -764,26 +782,10 @@ export function TimelineProvider({
|
||||
allocsByResource,
|
||||
projectGroups,
|
||||
openDemandsByProject,
|
||||
viewStart,
|
||||
viewEnd,
|
||||
viewDays,
|
||||
setViewStart,
|
||||
setViewDays,
|
||||
filters,
|
||||
setFilters,
|
||||
filterOpen,
|
||||
setFilterOpen,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
today,
|
||||
displayMode,
|
||||
heatmapScheme,
|
||||
blinkOverbookedDays,
|
||||
isLoading,
|
||||
isInitialLoading,
|
||||
isEntriesError,
|
||||
totalAllocCount,
|
||||
activeFilterCount,
|
||||
}),
|
||||
[
|
||||
assignments,
|
||||
@@ -796,23 +798,44 @@ export function TimelineProvider({
|
||||
allocsByResource,
|
||||
projectGroups,
|
||||
openDemandsByProject,
|
||||
viewStart,
|
||||
viewEnd,
|
||||
viewDays,
|
||||
filters,
|
||||
filterOpen,
|
||||
viewMode,
|
||||
today,
|
||||
displayMode,
|
||||
heatmapScheme,
|
||||
blinkOverbookedDays,
|
||||
isLoading,
|
||||
isInitialLoading,
|
||||
isEntriesError,
|
||||
totalAllocCount,
|
||||
activeFilterCount,
|
||||
],
|
||||
);
|
||||
|
||||
return <TimelineContext.Provider value={value}>{children}</TimelineContext.Provider>;
|
||||
const viewValue = useMemo<TimelineViewContextValue>(
|
||||
() => ({
|
||||
viewStart,
|
||||
viewEnd,
|
||||
viewDays,
|
||||
setViewStart,
|
||||
setViewDays,
|
||||
filters,
|
||||
setFilters,
|
||||
filterOpen,
|
||||
setFilterOpen,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
today,
|
||||
activeFilterCount,
|
||||
}),
|
||||
[viewStart, viewEnd, viewDays, filters, filterOpen, viewMode, today, activeFilterCount],
|
||||
);
|
||||
|
||||
const displayValue = useMemo<TimelineDisplayContextValue>(
|
||||
() => ({ displayMode, heatmapScheme, blinkOverbookedDays }),
|
||||
[displayMode, heatmapScheme, blinkOverbookedDays],
|
||||
);
|
||||
|
||||
return (
|
||||
<TimelineDataContext.Provider value={dataValue}>
|
||||
<TimelineViewContext.Provider value={viewValue}>
|
||||
<TimelineDisplayContext.Provider value={displayValue}>
|
||||
{children}
|
||||
</TimelineDisplayContext.Provider>
|
||||
</TimelineViewContext.Provider>
|
||||
</TimelineDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useStat
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import type { CSSProperties } from "react";
|
||||
import {
|
||||
useTimelineContext,
|
||||
useTimelineData,
|
||||
useTimelineView,
|
||||
useTimelineDisplay,
|
||||
type TimelineAssignmentEntry,
|
||||
type TimelineDemandEntry,
|
||||
} from "./TimelineContext.js";
|
||||
@@ -32,7 +34,12 @@ import {
|
||||
ORDER_TYPE_COLORS,
|
||||
} from "./timelineConstants.js";
|
||||
import { getProjectColor } from "~/lib/project-colors.js";
|
||||
import type { DragState, AllocDragState, RangeState, MultiSelectState } from "~/hooks/useTimelineDrag.js";
|
||||
import type {
|
||||
DragState,
|
||||
AllocDragState,
|
||||
RangeState,
|
||||
MultiSelectState,
|
||||
} from "~/hooks/useTimelineDrag.js";
|
||||
import type { AllocMouseDownInfo, RowMouseDownInfo } from "./TimelineResourcePanel.js";
|
||||
import {
|
||||
buildVacationBlocksByResource,
|
||||
@@ -50,10 +57,7 @@ import {
|
||||
} from "./timelineHover.js";
|
||||
import { buildResourceHeatmapSeries } from "./timelineHeatmap.js";
|
||||
import { buildResourceCapacitySeries } from "./timelineCapacity.js";
|
||||
import {
|
||||
buildProjectRowMetrics,
|
||||
type ProjectDayMetric,
|
||||
} from "./timelineProjectMetrics.js";
|
||||
import { buildProjectRowMetrics, type ProjectDayMetric } from "./timelineProjectMetrics.js";
|
||||
import {
|
||||
buildProjectFlatRows,
|
||||
estimateProjectRowHeight,
|
||||
@@ -147,18 +151,10 @@ function TimelineProjectPanelInner({
|
||||
gridLines,
|
||||
xToDate,
|
||||
}: TimelineProjectPanelProps) {
|
||||
const {
|
||||
projectGroups,
|
||||
openDemandsByProject,
|
||||
allocsByResource,
|
||||
vacationsByResource,
|
||||
filters,
|
||||
displayMode,
|
||||
heatmapScheme,
|
||||
blinkOverbookedDays,
|
||||
activeFilterCount,
|
||||
today,
|
||||
} = useTimelineContext();
|
||||
const { projectGroups, openDemandsByProject, allocsByResource, vacationsByResource } =
|
||||
useTimelineData();
|
||||
const { filters, activeFilterCount, today } = useTimelineView();
|
||||
const { displayMode, heatmapScheme, blinkOverbookedDays } = useTimelineDisplay();
|
||||
|
||||
const visualAllocsByResource = useMemo(() => {
|
||||
if (optimisticAllocations.size === 0) return allocsByResource;
|
||||
@@ -171,13 +167,14 @@ function TimelineProjectPanelInner({
|
||||
}, [allocsByResource, optimisticAllocations]);
|
||||
|
||||
const visualProjectGroups = useMemo(
|
||||
() => projectGroups.map((project) => ({
|
||||
...project,
|
||||
resourceRows: project.resourceRows.map((row) => ({
|
||||
...row,
|
||||
allocs: applyVisualOverrides(row.allocs, optimisticAllocations),
|
||||
() =>
|
||||
projectGroups.map((project) => ({
|
||||
...project,
|
||||
resourceRows: project.resourceRows.map((row) => ({
|
||||
...row,
|
||||
allocs: applyVisualOverrides(row.allocs, optimisticAllocations),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
[projectGroups, optimisticAllocations],
|
||||
);
|
||||
|
||||
@@ -224,7 +221,15 @@ function TimelineProjectPanelInner({
|
||||
totalCanvasWidth,
|
||||
filters.showWeekends,
|
||||
),
|
||||
[CELL_WIDTH, filters.showVacations, filters.showWeekends, toLeft, toWidth, totalCanvasWidth, vacationsByResource],
|
||||
[
|
||||
CELL_WIDTH,
|
||||
filters.showVacations,
|
||||
filters.showWeekends,
|
||||
toLeft,
|
||||
toWidth,
|
||||
totalCanvasWidth,
|
||||
vacationsByResource,
|
||||
],
|
||||
);
|
||||
|
||||
const projectRowMetrics = useMemo(() => {
|
||||
@@ -262,10 +267,7 @@ function TimelineProjectPanelInner({
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const dayIndex = Math.floor((e.clientX - rect.left) / CELL_WIDTH);
|
||||
if (
|
||||
dayIndex === lastHeatmapDayRef.current &&
|
||||
resourceId === lastHeatmapResourceRef.current
|
||||
)
|
||||
if (dayIndex === lastHeatmapDayRef.current && resourceId === lastHeatmapResourceRef.current)
|
||||
return;
|
||||
|
||||
pendingHeatmapRef.current = { clientX: e.clientX, rect, resourceId };
|
||||
@@ -310,7 +312,14 @@ function TimelineProjectPanelInner({
|
||||
return;
|
||||
}
|
||||
|
||||
updateTooltipPosition(vacationTooltipPosRef, vacationTooltipRef, e.clientX, e.clientY, 14, -8);
|
||||
updateTooltipPosition(
|
||||
vacationTooltipPosRef,
|
||||
vacationTooltipRef,
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
14,
|
||||
-8,
|
||||
);
|
||||
scheduleVacationHoverUpdate({
|
||||
frameRef: vacationHoverRafRef,
|
||||
hoveredKeyRef: hoveredVacationKeyRef,
|
||||
@@ -350,16 +359,13 @@ function TimelineProjectPanelInner({
|
||||
}
|
||||
}, [demandHover]);
|
||||
|
||||
const handleDemandHoverMove = useCallback(
|
||||
(e: React.MouseEvent, demand: TimelineDemandEntry) => {
|
||||
updateTooltipPosition(demandTooltipPosRef, demandTooltipRef, e.clientX, e.clientY, 16, -36);
|
||||
const handleDemandHoverMove = useCallback((e: React.MouseEvent, demand: TimelineDemandEntry) => {
|
||||
updateTooltipPosition(demandTooltipPosRef, demandTooltipRef, e.clientX, e.clientY, 16, -36);
|
||||
|
||||
startTransition(() => {
|
||||
setDemandHover(buildDemandHoverData(demand));
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
startTransition(() => {
|
||||
setDemandHover(buildDemandHoverData(demand));
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -432,8 +438,14 @@ function TimelineProjectPanelInner({
|
||||
return (
|
||||
<div
|
||||
data-project-group="true"
|
||||
className={clsx("flex border-b border-gray-200 dark:border-gray-700 group/proj", colors.light)}
|
||||
style={{ height: PROJECT_HEADER_HEIGHT, borderLeft: `4px solid ${customColor ?? projectColor.hex}` }}
|
||||
className={clsx(
|
||||
"flex border-b border-gray-200 dark:border-gray-700 group/proj",
|
||||
colors.light,
|
||||
)}
|
||||
style={{
|
||||
height: PROJECT_HEADER_HEIGHT,
|
||||
borderLeft: `4px solid ${customColor ?? projectColor.hex}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -570,7 +582,9 @@ function TimelineProjectPanelInner({
|
||||
<div className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate cursor-pointer hover:text-brand-600 dark:hover:text-brand-400 transition-colors">
|
||||
{row.resource.displayName}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500 truncate">{row.resource.eid}</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500 truncate">
|
||||
{row.resource.eid}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -721,7 +735,9 @@ function renderOpenDemandRow(
|
||||
?
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-amber-700 dark:text-amber-400 truncate">Open demand</div>
|
||||
<div className="text-xs font-medium text-amber-700 dark:text-amber-400 truncate">
|
||||
Open demand
|
||||
</div>
|
||||
<div className="text-[10px] text-amber-500 dark:text-amber-600 truncate">
|
||||
{openDemandCount} open demand{openDemandCount > 1 ? "s" : ""}
|
||||
</div>
|
||||
@@ -741,19 +757,23 @@ function renderOpenDemandRow(
|
||||
const allocStart = new Date(alloc.startDate);
|
||||
const allocEnd = new Date(alloc.endDate);
|
||||
|
||||
const isAllocDragged = allocDragState.isActive && allocDragState.allocationId === alloc.id;
|
||||
const isAllocDragged =
|
||||
allocDragState.isActive && allocDragState.allocationId === alloc.id;
|
||||
const dispStart =
|
||||
isAllocDragged && allocDragState.currentStartDate
|
||||
? allocDragState.currentStartDate
|
||||
: allocStart;
|
||||
const dispEnd =
|
||||
isAllocDragged && allocDragState.currentEndDate ? allocDragState.currentEndDate : allocEnd;
|
||||
isAllocDragged && allocDragState.currentEndDate
|
||||
? allocDragState.currentEndDate
|
||||
: allocEnd;
|
||||
|
||||
// Multi-drag visual offset
|
||||
const isMultiDragTarget =
|
||||
multiSelectState.isMultiDragging &&
|
||||
selectedAllocationSet.has(alloc.id);
|
||||
const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0;
|
||||
multiSelectState.isMultiDragging && selectedAllocationSet.has(alloc.id);
|
||||
const multiDragPx = isMultiDragTarget
|
||||
? multiSelectState.multiDragDaysDelta * CELL_WIDTH
|
||||
: 0;
|
||||
const multiDragMode = multiSelectState.multiDragMode;
|
||||
|
||||
let left = toLeft(dispStart);
|
||||
@@ -838,9 +858,12 @@ function renderOpenDemandRow(
|
||||
border: `2px dashed ${roleColor}B3`,
|
||||
...((multiDragPx && multiDragMode === "move") || dragTransform
|
||||
? {
|
||||
transform: [dragTransform, multiDragPx && multiDragMode === "move"
|
||||
? `translateX(${multiDragPx}px)`
|
||||
: null]
|
||||
transform: [
|
||||
dragTransform,
|
||||
multiDragPx && multiDragMode === "move"
|
||||
? `translateX(${multiDragPx}px)`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
}
|
||||
@@ -964,18 +987,18 @@ function renderProjectUtilOverlay(
|
||||
const projPct = (projH / capacityH) * 100;
|
||||
const totalPct = (totalH / capacityH) * 100;
|
||||
const projColor = useHeatmapColors
|
||||
? heatmapColor(
|
||||
? (heatmapColor(
|
||||
projPct,
|
||||
heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
|
||||
"bar",
|
||||
) ?? "rgba(59,130,246,0.8)"
|
||||
) ?? "rgba(59,130,246,0.8)")
|
||||
: "rgba(96,165,250,0.8)";
|
||||
const totalColor = useHeatmapColors
|
||||
? heatmapColor(
|
||||
? (heatmapColor(
|
||||
totalPct,
|
||||
heatmapScheme as import("~/hooks/useAppPreferences.js").HeatmapColorScheme,
|
||||
"bar",
|
||||
) ?? "rgba(156,163,175,0.5)"
|
||||
) ?? "rgba(156,163,175,0.5)")
|
||||
: isOver
|
||||
? "rgba(252,211,77,0.8)"
|
||||
: "rgba(209,213,219,0.8)";
|
||||
@@ -1081,8 +1104,7 @@ function renderProjectDragHandles(
|
||||
|
||||
// Multi-drag visual offset
|
||||
const isMultiDragTarget =
|
||||
multiSelectState.isMultiDragging &&
|
||||
selectedAllocationSet.has(alloc.id);
|
||||
multiSelectState.isMultiDragging && selectedAllocationSet.has(alloc.id);
|
||||
const multiDragPx = isMultiDragTarget ? multiSelectState.multiDragDaysDelta * CELL_WIDTH : 0;
|
||||
const multiDragMode = multiSelectState.multiDragMode;
|
||||
|
||||
@@ -1132,9 +1154,7 @@ function renderProjectDragHandles(
|
||||
? {
|
||||
transform: [
|
||||
dragTransform,
|
||||
multiDragPx && multiDragMode === "move"
|
||||
? `translateX(${multiDragPx}px)`
|
||||
: null,
|
||||
multiDragPx && multiDragMode === "move" ? `translateX(${multiDragPx}px)` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import { clsx } from "clsx";
|
||||
import { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useTimelineContext, type TimelineAssignmentEntry } from "./TimelineContext.js";
|
||||
import {
|
||||
useTimelineData,
|
||||
useTimelineView,
|
||||
useTimelineDisplay,
|
||||
type TimelineAssignmentEntry,
|
||||
} from "./TimelineContext.js";
|
||||
import { applyVisualOverrides, type TimelineVisualOverrides } from "./allocationVisualState.js";
|
||||
import { ConflictOverlay } from "./ConflictOverlay.js";
|
||||
import { computeSubLanes } from "./utils.js";
|
||||
@@ -117,18 +122,9 @@ function TimelineResourcePanelInner({
|
||||
gridLines,
|
||||
xToDate,
|
||||
}: TimelineResourcePanelProps) {
|
||||
const {
|
||||
resources,
|
||||
allocsByResource,
|
||||
vacationsByResource,
|
||||
filters,
|
||||
viewStart,
|
||||
viewEnd,
|
||||
displayMode,
|
||||
heatmapScheme,
|
||||
blinkOverbookedDays,
|
||||
activeFilterCount,
|
||||
} = useTimelineContext();
|
||||
const { resources, allocsByResource, vacationsByResource } = useTimelineData();
|
||||
const { filters, viewStart, viewEnd, activeFilterCount } = useTimelineView();
|
||||
const { displayMode, heatmapScheme, blinkOverbookedDays } = useTimelineDisplay();
|
||||
|
||||
// ─── Heatmap hover state ────────────────────────────────────────────────────
|
||||
const heatmapRafRef = useRef<number | null>(null);
|
||||
|
||||
@@ -28,7 +28,8 @@ import { HEADER_DAY_HEIGHT, HEADER_MONTH_HEIGHT, LABEL_WIDTH } from "./timelineC
|
||||
import { formatDateShort } from "~/lib/format.js";
|
||||
import {
|
||||
TimelineProvider,
|
||||
useTimelineContext,
|
||||
useTimelineData,
|
||||
useTimelineView,
|
||||
type TimelineAssignmentEntry,
|
||||
} from "./TimelineContext.js";
|
||||
import { TimelineResourcePanel } from "./TimelineResourcePanel.js";
|
||||
@@ -339,17 +340,22 @@ function TimelineViewContent({
|
||||
undo: () => Promise<void>;
|
||||
redo: () => Promise<void>;
|
||||
}) {
|
||||
const ctx = useTimelineContext();
|
||||
const {
|
||||
resources,
|
||||
projectGroups,
|
||||
allocsByResource,
|
||||
openDemandsByProject,
|
||||
visibleAssignments,
|
||||
visibleDemands,
|
||||
isLoading,
|
||||
isInitialLoading,
|
||||
isEntriesError,
|
||||
totalAllocCount,
|
||||
} = useTimelineData();
|
||||
const {
|
||||
viewStart,
|
||||
viewEnd,
|
||||
viewDays,
|
||||
visibleAssignments,
|
||||
visibleDemands,
|
||||
setViewStart,
|
||||
setViewDays,
|
||||
filters,
|
||||
@@ -359,11 +365,7 @@ function TimelineViewContent({
|
||||
viewMode,
|
||||
setViewMode,
|
||||
today,
|
||||
isLoading,
|
||||
isInitialLoading,
|
||||
isEntriesError,
|
||||
totalAllocCount,
|
||||
} = ctx;
|
||||
} = useTimelineView();
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
Reference in New Issue
Block a user