fix: hover card, preferences modal, timeline scroll, multi-drag
- ResourceHoverCard: add isInitialLoading to useEffect deps so mouseover/mouseout listeners attach after canvas mounts - PreferencesModal: lift prefsOpen state to AppShell, render modal outside sidebar's backdrop-blur stacking context - Timeline page: constrain to max-h-[100dvh] overflow-hidden so horizontal scrollbar is accessible without scrolling to bottom - Multi-drag: pass selectedAllocationIds from ref at drag completion to prevent stale closure in onMultiDragComplete callback Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -15,7 +15,7 @@ const TimelineView = dynamic(
|
|||||||
|
|
||||||
export default function TimelinePage() {
|
export default function TimelinePage() {
|
||||||
return (
|
return (
|
||||||
<div className="app-page flex h-full flex-col gap-5 pb-6">
|
<div className="app-page flex max-h-[100dvh] flex-col gap-5 overflow-hidden pb-6">
|
||||||
<div className="app-page-header">
|
<div className="app-page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="app-page-title">Timeline</h1>
|
<h1 className="app-page-title">Timeline</h1>
|
||||||
|
|||||||
@@ -295,15 +295,16 @@ function SidebarContent({
|
|||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
onNavClick,
|
onNavClick,
|
||||||
|
onPrefsOpen,
|
||||||
}: {
|
}: {
|
||||||
userRole: string;
|
userRole: string;
|
||||||
onChatOpen: () => void;
|
onChatOpen: () => void;
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
onToggleCollapse: () => void;
|
onToggleCollapse: () => void;
|
||||||
onNavClick?: () => void;
|
onNavClick?: () => void;
|
||||||
|
onPrefsOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [prefsOpen, setPrefsOpen] = useState(false);
|
|
||||||
|
|
||||||
const activeHrefSet = useMemo(() => {
|
const activeHrefSet = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
@@ -594,7 +595,7 @@ function SidebarContent({
|
|||||||
<NavTooltip label="Preferences" show={sidebarCollapsed}>
|
<NavTooltip label="Preferences" show={sidebarCollapsed}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPrefsOpen(true)}
|
onClick={onPrefsOpen}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex w-full items-center rounded-2xl text-sm text-gray-700 transition-colors hover:bg-gray-100/90 dark:text-gray-300 dark:hover:bg-slate-900",
|
"flex w-full items-center rounded-2xl text-sm text-gray-700 transition-colors hover:bg-gray-100/90 dark:text-gray-300 dark:hover:bg-slate-900",
|
||||||
sidebarCollapsed ? "justify-center px-2 py-2.5" : "gap-3 px-3 py-2.5",
|
sidebarCollapsed ? "justify-center px-2 py-2.5" : "gap-3 px-3 py-2.5",
|
||||||
@@ -646,7 +647,6 @@ function SidebarContent({
|
|||||||
</NavTooltip>
|
</NavTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{prefsOpen && <PreferencesModal onClose={() => setPrefsOpen(false)} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -660,11 +660,13 @@ function DesktopSidebar({
|
|||||||
onChatOpen,
|
onChatOpen,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
|
onPrefsOpen,
|
||||||
}: {
|
}: {
|
||||||
userRole: string;
|
userRole: string;
|
||||||
onChatOpen: () => void;
|
onChatOpen: () => void;
|
||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
onToggleCollapse: () => void;
|
onToggleCollapse: () => void;
|
||||||
|
onPrefsOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
@@ -679,6 +681,7 @@ function DesktopSidebar({
|
|||||||
onChatOpen={onChatOpen}
|
onChatOpen={onChatOpen}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
onToggleCollapse={onToggleCollapse}
|
onToggleCollapse={onToggleCollapse}
|
||||||
|
onPrefsOpen={onPrefsOpen}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
@@ -693,11 +696,13 @@ function MobileSidebar({
|
|||||||
onClose,
|
onClose,
|
||||||
userRole,
|
userRole,
|
||||||
onChatOpen,
|
onChatOpen,
|
||||||
|
onPrefsOpen,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
userRole: string;
|
userRole: string;
|
||||||
onChatOpen: () => void;
|
onChatOpen: () => void;
|
||||||
|
onPrefsOpen: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -738,6 +743,10 @@ function MobileSidebar({
|
|||||||
sidebarCollapsed={false}
|
sidebarCollapsed={false}
|
||||||
onToggleCollapse={() => {}}
|
onToggleCollapse={() => {}}
|
||||||
onNavClick={onClose}
|
onNavClick={onClose}
|
||||||
|
onPrefsOpen={() => {
|
||||||
|
onPrefsOpen();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</motion.nav>
|
</motion.nav>
|
||||||
</>
|
</>
|
||||||
@@ -754,6 +763,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
|||||||
const [chatOpen, setChatOpen] = useState(false);
|
const [chatOpen, setChatOpen] = useState(false);
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const contentRef = useRef<HTMLElement>(null);
|
const contentRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
@@ -802,6 +812,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
|||||||
onChatOpen={() => setChatOpen(true)}
|
onChatOpen={() => setChatOpen(true)}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
onToggleCollapse={handleToggleCollapse}
|
onToggleCollapse={handleToggleCollapse}
|
||||||
|
onPrefsOpen={() => setPrefsOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mobile sidebar overlay */}
|
{/* Mobile sidebar overlay */}
|
||||||
@@ -810,6 +821,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
|||||||
onClose={() => setMobileOpen(false)}
|
onClose={() => setMobileOpen(false)}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
onChatOpen={() => setChatOpen(true)}
|
onChatOpen={() => setChatOpen(true)}
|
||||||
|
onPrefsOpen={() => setPrefsOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
@@ -844,6 +856,7 @@ export function AppShell({ children, userRole = "USER" }: { children: React.Reac
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{prefsOpen && <PreferencesModal onClose={() => setPrefsOpen(false)} />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ export function TimelineView() {
|
|||||||
return { ...prev, isSelecting: false, selectedAllocationIds: [...ids] };
|
return { ...prev, isSelecting: false, selectedAllocationIds: [...ids] };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onMultiDragComplete: (daysDelta, mode) => {
|
onMultiDragComplete: (daysDelta, mode, selectedIds) => {
|
||||||
const ids = multiSelectState.selectedAllocationIds;
|
const ids = selectedIds ?? multiSelectState.selectedAllocationIds;
|
||||||
if (ids.length > 0 && daysDelta !== 0) {
|
if (ids.length > 0 && daysDelta !== 0) {
|
||||||
pushBatchHistoryRef.current(ids, daysDelta, mode);
|
pushBatchHistoryRef.current(ids, daysDelta, mode);
|
||||||
batchShiftMutationOuter.mutate({ allocationIds: ids, daysDelta, mode });
|
batchShiftMutationOuter.mutate({ allocationIds: ids, daysDelta, mode });
|
||||||
@@ -547,7 +547,7 @@ function TimelineViewContent({
|
|||||||
resourceHoverTimerRef.current = null;
|
resourceHoverTimerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [resourceHover?.resourceId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [resourceHover?.resourceId, isInitialLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// ─── Lazy-extend date range on scroll ─────────────────────────────────────
|
// ─── Lazy-extend date range on scroll ─────────────────────────────────────
|
||||||
function handleContainerScroll() {
|
function handleContainerScroll() {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export function useTimelineDrag({
|
|||||||
onRangeSelected?: (info: RangeSelectedInfo) => void;
|
onRangeSelected?: (info: RangeSelectedInfo) => void;
|
||||||
onAllocationMoved?: (snapshot: AllocationMovedSnapshot) => void;
|
onAllocationMoved?: (snapshot: AllocationMovedSnapshot) => void;
|
||||||
onShiftClickAlloc?: (allocationId: string) => void;
|
onShiftClickAlloc?: (allocationId: string) => void;
|
||||||
onMultiDragComplete?: (daysDelta: number, mode: AllocDragMode) => void;
|
onMultiDragComplete?: (daysDelta: number, mode: AllocDragMode, selectedIds?: string[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [dragState, setDragState] = useState<DragState>(INITIAL_DRAG_STATE);
|
const [dragState, setDragState] = useState<DragState>(INITIAL_DRAG_STATE);
|
||||||
const [allocDragState, setAllocDragState] = useState<AllocDragState>(INITIAL_ALLOC_DRAG);
|
const [allocDragState, setAllocDragState] = useState<AllocDragState>(INITIAL_ALLOC_DRAG);
|
||||||
@@ -394,7 +394,9 @@ export function useTimelineDrag({
|
|||||||
multiSelectRef.current = { ...multiSelectRef.current, isMultiDragging: false, multiDragDaysDelta: 0 };
|
multiSelectRef.current = { ...multiSelectRef.current, isMultiDragging: false, multiDragDaysDelta: 0 };
|
||||||
|
|
||||||
if (finalDelta !== 0) {
|
if (finalDelta !== 0) {
|
||||||
onMultiDragCompleteRef.current?.(finalDelta, dragMode);
|
// Pass IDs from ref to avoid stale closure in the callback
|
||||||
|
const ids = multiSelectRef.current.selectedAllocationIds;
|
||||||
|
onMultiDragCompleteRef.current?.(finalDelta, dragMode, ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user