Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 85 additions & 18 deletions src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { eachDayOfInterval, isMonday, differenceInDays } from 'date-fns';
import { getMonday } from '../../../utils/datetime.utils';
import { toDateString } from 'shared';
import { GANTT_CHART_CELL_SIZE, GANTT_CHART_GAP_SIZE } from '../../../utils/gantt.utils';
import { useRef, useCallback, useEffect } from 'react';

export interface GanttEditability<E, T> {
highlightTaskComparator: HighlightTaskComparator<T>;
highlightSubtaskComparator: HighlightTaskComparator<T>;
Expand All @@ -38,38 +40,103 @@ const GanttChart = <E, T>({ startDate, endDate, collections, editability }: Gant

const today = new Date(new Date().setHours(0, 0, 0, 0));
const currentWeekCol = days.findIndex((day) => toDateString(day) === toDateString(getMonday(today))) + 1;

const daysIntoWeek = differenceInDays(today, getMonday(today));
const dailyOffset = daysIntoWeek * (parseFloat(GANTT_CHART_CELL_SIZE) / 7);

const scrollContainerRef = useRef<HTMLDivElement>(null);

const validCollections = collections.filter((c) => c.tasks);

const sectionDataRef = useRef<
{ sectionEl: HTMLDivElement | null; placeholderEl: HTMLDivElement | null; height: number }[]
>([]);

if (sectionDataRef.current.length !== validCollections.length) {
sectionDataRef.current = validCollections.map((_, i) => ({
sectionEl: sectionDataRef.current[i]?.sectionEl ?? null,
placeholderEl: sectionDataRef.current[i]?.placeholderEl ?? null,
height: sectionDataRef.current[i]?.height ?? 0
}));
}

const updateVisibility = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;

const containerRect = container.getBoundingClientRect();
const viewportBottom = containerRect.bottom;

for (const data of sectionDataRef.current) {
const { sectionEl, placeholderEl } = data;
if (!sectionEl || !placeholderEl) continue;

const el = sectionEl.style.display === 'none' ? placeholderEl : sectionEl;
const elTop = el.getBoundingClientRect().top;
const measuredHeight = sectionEl.offsetHeight || placeholderEl.offsetHeight;

const isVisible = elTop <= viewportBottom;

sectionEl.style.display = isVisible ? '' : 'none';
placeholderEl.style.display = isVisible ? 'none' : '';
placeholderEl.style.height = `${measuredHeight}px`;
}
}, []);

useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;

container.addEventListener('scroll', updateVisibility, { passive: true });
// Also re-check on container resize
const ro = new ResizeObserver(updateVisibility);
ro.observe(container);
updateVisibility(); // initial pass

return () => {
container.removeEventListener('scroll', updateVisibility);
ro.disconnect();
};
}, [updateVisibility]);

return (
<Box
ref={scrollContainerRef}
sx={{
width: '100%',
height: { xs: 'calc(100vh - 9.5rem )', md: 'calc(100vh - 6.25rem)' },
height: { xs: 'calc(100vh - 9.5rem)', md: 'calc(100vh - 6.25rem)' },
overflow: 'scroll',
position: 'relative',
'&::-webkit-scrollbar': {
display: 'none'
},
scrollbarWidth: 'none', // Firefox
msOverflowStyle: 'none' // IE and Edge
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
msOverflowStyle: 'none'
}}
>
<GanttChartTimeline start={startDate} end={endDate} />
<Box sx={{ position: 'relative' }}>
{collections.map((collection) => {
return collection.tasks ? (
<GanttChartCollectionSection
startDate={startDate}
endDate={endDate}
collection={collection}
editability={editability}
{validCollections.map((collection, idx) => (
<Box key={idx}>
{/* Real */}
<Box
ref={(el: HTMLDivElement | null) => {
if (sectionDataRef.current[idx]) sectionDataRef.current[idx].sectionEl = el;
}}
>
<GanttChartCollectionSection
startDate={startDate}
endDate={endDate}
collection={collection}
editability={editability}
/>
</Box>
{/* Placeholder */}
<Box
ref={(el: HTMLDivElement | null) => {
if (sectionDataRef.current[idx]) sectionDataRef.current[idx].placeholderEl = el;
}}
style={{ display: 'none', height: 0 }}
/>
) : (
<></>
);
})}
</Box>
))}

{currentWeekCol > 0 && (
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ import { Edit } from '@mui/icons-material';
import { Box, Chip, IconButton, Typography, useTheme } from '@mui/material';
import GanttChartSection from './GanttChartSection';
import { GanttCollection } from '../../../utils/gantt.utils';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { GanttEditability } from './GanttChart';

interface GanttChartCollectionSectionProps<E, T> {
startDate: Date;
endDate: Date;
collection: GanttCollection<E, T>;
editability?: GanttEditability<E, T>;
onHeightChange?: (height: number) => void;
}

const GanttChartCollectionSection = <E, T>({
startDate,
endDate,
collection,
editability
editability,
onHeightChange
}: GanttChartCollectionSectionProps<E, T>) => {
const theme = useTheme();
const [isEditMode, setIsEditMode] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);

const collectionSectionBackgroundStyle = {
mt: 1,
Expand Down Expand Up @@ -61,8 +64,20 @@ const GanttChartCollectionSection = <E, T>({

const ignoreBool = () => false;

useEffect(() => {
const el = sectionRef.current;
if (!el || !onHeightChange) return;

const ro = new ResizeObserver(() => {
onHeightChange(el.getBoundingClientRect().height);
});
ro.observe(el);
onHeightChange(el.getBoundingClientRect().height);
return () => ro.disconnect();
}, [onHeightChange]);

return (
<Box sx={collectionSectionBackgroundStyle}>
<Box ref={sectionRef} sx={collectionSectionBackgroundStyle}>
<Box sx={collectionDescriptionContainerStyle}>
<Typography variant="h6" fontWeight={400}>
{collection.title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
HighlightTaskComparator,
OnMouseOverOptions
} from '../../../../../utils/gantt.utils';
import { Collapse } from '@mui/material';
import { Box } from '@mui/material';
import GanttTaskBar from './GanttTaskBar';
import GanttTaskBarDisplay from './GanttTaskBarDisplay';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';

interface GanttTaskBarViewProps<T> {
days: Date[];
Expand Down Expand Up @@ -37,11 +37,19 @@ const GanttTaskBarView = <T,>({
onToggle
}: GanttTaskBarViewProps<T>) => {
const [showChildren, setShowChildren] = useState(false);
const animationRef = useRef<number | null>(null);

const handleToggle = () => {
setShowChildren((prev) => !prev);
};

// Fire onToggle after the grid animation completes (200ms matches transition below)
useEffect(() => {
if (animationRef.current) cancelAnimationFrame(animationRef.current);
const timeout = setTimeout(() => onToggle?.(), 200);
return () => clearTimeout(timeout);
}, [showChildren, onToggle]);

return (
<>
<GanttTaskBarDisplay
Expand All @@ -57,25 +65,33 @@ const GanttTaskBarView = <T,>({
highlightSubtaskComparator={highlightSubtaskComparator}
highlightTaskComparator={highlightTaskComparator}
/>

<Collapse in={showChildren} unmountOnExit onEntered={onToggle} onExited={onToggle}>
{task.children.map((child) => (
<GanttTaskBar
key={child.id}
days={days}
task={child}
isEditMode={false}
createChange={() => {}}
handleOnMouseOver={handleOnMouseOver}
handleOnMouseLeave={handleOnMouseLeave}
highlightedChange={highlightedChange}
onAddTaskPressed={onAddTaskPressed}
highlightSubtaskComparator={highlightSubtaskComparator}
highlightTaskComparator={highlightTaskComparator}
onToggle={onToggle}
/>
))}
</Collapse>
<Box
sx={{
display: 'grid',
gridTemplateRows: showChildren ? '1fr' : '0fr',
transition: 'grid-template-rows 200ms ease',
overflow: 'hidden'
}}
>
<Box sx={{ minHeight: 0 }}>
{task.children.map((child) => (
<GanttTaskBar
key={child.id}
days={days}
task={child}
isEditMode={false}
createChange={() => {}}
handleOnMouseOver={handleOnMouseOver}
handleOnMouseLeave={handleOnMouseLeave}
highlightedChange={highlightedChange}
onAddTaskPressed={onAddTaskPressed}
highlightSubtaskComparator={highlightSubtaskComparator}
highlightTaskComparator={highlightTaskComparator}
onToggle={onToggle}
/>
))}
</Box>
</Box>
</>
);
};
Expand Down
Loading