2020-08-29 14:01:04 +03:00
|
|
|
import React, { useState, SyntheticEvent, useRef, useEffect } from "react";
|
2021-03-01 21:55:27 +02:00
|
|
|
import { ViewMode, GanttProps } from "../../types/public-types";
|
2020-08-23 22:33:25 +03:00
|
|
|
import { GridProps } from "../grid/grid";
|
2020-08-05 08:14:22 +03:00
|
|
|
import { ganttDateRange, seedDates } from "../../helpers/date-helper";
|
2020-08-23 22:33:25 +03:00
|
|
|
import { CalendarProps } from "../calendar/calendar";
|
|
|
|
|
import { TaskGanttContentProps } from "./task-gantt-content";
|
|
|
|
|
import { TaskListHeaderDefault } from "../task-list/task-list-header";
|
|
|
|
|
import { TaskListTableDefault } from "../task-list/task-list-table";
|
|
|
|
|
import { StandardTooltipContent } from "../other/tooltip";
|
|
|
|
|
import { Scroll } from "../other/scroll";
|
|
|
|
|
import { TaskListProps, TaskList } from "../task-list/task-list";
|
|
|
|
|
import { TaskGantt } from "./task-gantt";
|
2021-03-01 21:55:27 +02:00
|
|
|
import { BarTask } from "../../types/bar-task";
|
|
|
|
|
import { convertToBarTasks } from "../../helpers/bar-helper";
|
|
|
|
|
import { GanttEvent } from "../../types/gantt-task-actions";
|
2021-03-21 16:58:20 +02:00
|
|
|
import { DateSetup } from "../../types/date-setup";
|
2021-03-25 23:06:20 +02:00
|
|
|
import styles from "./gantt.module.css";
|
2020-07-22 20:50:43 +03:00
|
|
|
|
2021-03-01 21:55:27 +02:00
|
|
|
export const Gantt: React.FunctionComponent<GanttProps> = ({
|
2020-07-22 20:50:43 +03:00
|
|
|
tasks,
|
|
|
|
|
headerHeight = 50,
|
|
|
|
|
columnWidth = 60,
|
2020-09-01 23:08:15 +03:00
|
|
|
listCellWidth = "155px",
|
2020-07-22 20:50:43 +03:00
|
|
|
rowHeight = 50,
|
2020-08-23 22:33:25 +03:00
|
|
|
ganttHeight = 0,
|
2020-07-22 20:50:43 +03:00
|
|
|
viewMode = ViewMode.Day,
|
2020-08-05 08:14:22 +03:00
|
|
|
locale = "en-GB",
|
2020-07-22 20:50:43 +03:00
|
|
|
barFill = 60,
|
|
|
|
|
barCornerRadius = 3,
|
2020-08-05 08:14:22 +03:00
|
|
|
barProgressColor = "#a3a3ff",
|
|
|
|
|
barProgressSelectedColor = "#8282f5",
|
|
|
|
|
barBackgroundColor = "#b8c2cc",
|
|
|
|
|
barBackgroundSelectedColor = "#aeb8c2",
|
2021-03-01 21:55:27 +02:00
|
|
|
milestoneBackgroundColor = "#f1c453",
|
|
|
|
|
milestoneBackgroundSelectedColor = "#f29e4c",
|
2020-07-22 20:50:43 +03:00
|
|
|
handleWidth = 8,
|
|
|
|
|
timeStep = 300000,
|
2020-08-05 08:14:22 +03:00
|
|
|
arrowColor = "grey",
|
|
|
|
|
fontFamily = "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue",
|
|
|
|
|
fontSize = "14px",
|
2020-07-22 20:50:43 +03:00
|
|
|
arrowIndent = 20,
|
2020-08-05 08:14:22 +03:00
|
|
|
todayColor = "rgba(252, 248, 227, 0.5)",
|
2020-08-23 22:33:25 +03:00
|
|
|
TooltipContent = StandardTooltipContent,
|
|
|
|
|
TaskListHeader = TaskListHeaderDefault,
|
|
|
|
|
TaskListTable = TaskListTableDefault,
|
2020-07-22 20:50:43 +03:00
|
|
|
onDateChange,
|
|
|
|
|
onProgressChange,
|
|
|
|
|
onDoubleClick,
|
|
|
|
|
onTaskDelete,
|
2020-09-09 17:08:16 +03:00
|
|
|
onSelect,
|
2020-07-22 20:50:43 +03:00
|
|
|
}) => {
|
2020-08-29 14:01:04 +03:00
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
2021-03-21 16:58:20 +02:00
|
|
|
const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
|
2021-03-01 21:55:27 +02:00
|
|
|
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
|
2021-03-21 16:58:20 +02:00
|
|
|
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
|
2021-03-01 21:55:27 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [taskHeight, setTaskHeight] = useState((rowHeight * barFill) / 100);
|
|
|
|
|
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
|
|
|
|
|
const [ganttEvent, setGanttEvent] = useState<GanttEvent>({
|
|
|
|
|
action: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [selectedTask, setSelectedTask] = useState<BarTask>();
|
|
|
|
|
const [failedTask, setFailedTask] = useState<BarTask | null>(null);
|
2021-03-25 23:06:20 +02:00
|
|
|
|
2020-08-24 23:47:22 +03:00
|
|
|
const [scrollY, setScrollY] = useState(0);
|
2020-08-25 20:58:36 +03:00
|
|
|
const [scrollX, setScrollX] = useState(0);
|
2020-08-29 14:01:04 +03:00
|
|
|
const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false);
|
2020-08-11 01:16:53 +03:00
|
|
|
|
2021-03-01 21:55:27 +02:00
|
|
|
const svgHeight = rowHeight * barTasks.length;
|
2021-03-25 23:06:20 +02:00
|
|
|
const svgWidth = dateSetup.dates.length * columnWidth;
|
2021-03-01 21:55:27 +02:00
|
|
|
const ganttFullHeight = barTasks.length * rowHeight;
|
2020-08-23 22:33:25 +03:00
|
|
|
|
2021-03-01 21:55:27 +02:00
|
|
|
// task change events
|
2020-08-31 23:26:44 +03:00
|
|
|
useEffect(() => {
|
2021-03-01 21:55:27 +02:00
|
|
|
const [startDate, endDate] = ganttDateRange(tasks, viewMode);
|
|
|
|
|
const newDates = seedDates(startDate, endDate, viewMode);
|
2021-03-21 16:58:20 +02:00
|
|
|
setDateSetup({ dates: newDates, viewMode });
|
2021-03-01 21:55:27 +02:00
|
|
|
setBarTasks(
|
|
|
|
|
convertToBarTasks(
|
|
|
|
|
tasks,
|
|
|
|
|
newDates,
|
|
|
|
|
columnWidth,
|
|
|
|
|
rowHeight,
|
|
|
|
|
taskHeight,
|
|
|
|
|
barCornerRadius,
|
|
|
|
|
handleWidth,
|
|
|
|
|
barProgressColor,
|
|
|
|
|
barProgressSelectedColor,
|
|
|
|
|
barBackgroundColor,
|
|
|
|
|
barBackgroundSelectedColor,
|
|
|
|
|
milestoneBackgroundColor,
|
|
|
|
|
milestoneBackgroundSelectedColor
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}, [
|
|
|
|
|
tasks,
|
|
|
|
|
viewMode,
|
|
|
|
|
rowHeight,
|
|
|
|
|
barCornerRadius,
|
|
|
|
|
columnWidth,
|
|
|
|
|
taskHeight,
|
|
|
|
|
handleWidth,
|
|
|
|
|
barProgressColor,
|
|
|
|
|
barProgressSelectedColor,
|
|
|
|
|
barBackgroundColor,
|
|
|
|
|
barBackgroundSelectedColor,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const { changedTask, action } = ganttEvent;
|
|
|
|
|
if (changedTask) {
|
|
|
|
|
if (action === "delete") {
|
|
|
|
|
setGanttEvent({ action: "" });
|
|
|
|
|
setBarTasks(barTasks.filter(t => t.id !== changedTask.id));
|
|
|
|
|
} else if (
|
|
|
|
|
action === "move" ||
|
|
|
|
|
action === "end" ||
|
|
|
|
|
action === "start" ||
|
|
|
|
|
action === "progress"
|
|
|
|
|
) {
|
|
|
|
|
const prevStateTask = barTasks.find(t => t.id === changedTask.id);
|
|
|
|
|
if (
|
|
|
|
|
prevStateTask &&
|
|
|
|
|
(prevStateTask.start.getTime() !== changedTask.start.getTime() ||
|
|
|
|
|
prevStateTask.end.getTime() !== changedTask.end.getTime() ||
|
|
|
|
|
prevStateTask.progress !== changedTask.progress)
|
|
|
|
|
) {
|
|
|
|
|
// actions for change
|
|
|
|
|
const newTaskList = barTasks.map(t =>
|
|
|
|
|
t.id === changedTask.id ? changedTask : t
|
|
|
|
|
);
|
|
|
|
|
setBarTasks(newTaskList);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [ganttEvent, barTasks]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (failedTask) {
|
|
|
|
|
setBarTasks(barTasks.map(t => (t.id !== failedTask.id ? t : failedTask)));
|
|
|
|
|
setFailedTask(null);
|
|
|
|
|
}
|
|
|
|
|
}, [failedTask, barTasks]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const newTaskHeight = (rowHeight * barFill) / 100;
|
|
|
|
|
if (newTaskHeight !== taskHeight) {
|
|
|
|
|
setTaskHeight(newTaskHeight);
|
|
|
|
|
}
|
|
|
|
|
}, [rowHeight, barFill, taskHeight]);
|
2020-08-31 23:26:44 +03:00
|
|
|
|
2020-08-29 14:01:04 +03:00
|
|
|
// scroll events
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleWheel = (event: WheelEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const newScrollY = scrollY + event.deltaY;
|
|
|
|
|
if (newScrollY < 0) {
|
|
|
|
|
setScrollY(0);
|
|
|
|
|
} else if (newScrollY > ganttFullHeight - ganttHeight) {
|
|
|
|
|
setScrollY(ganttFullHeight - ganttHeight);
|
|
|
|
|
} else {
|
|
|
|
|
setScrollY(newScrollY);
|
|
|
|
|
}
|
|
|
|
|
setIgnoreScrollEvent(true);
|
|
|
|
|
};
|
2020-07-22 20:50:43 +03:00
|
|
|
|
2020-08-29 14:01:04 +03:00
|
|
|
// subscribe if scroll is necessary
|
|
|
|
|
if (
|
|
|
|
|
wrapperRef.current &&
|
|
|
|
|
ganttHeight &&
|
2021-03-01 21:55:27 +02:00
|
|
|
ganttHeight < barTasks.length * rowHeight
|
2020-08-29 14:01:04 +03:00
|
|
|
) {
|
|
|
|
|
wrapperRef.current.addEventListener("wheel", handleWheel, {
|
|
|
|
|
passive: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return () => {
|
|
|
|
|
if (wrapperRef.current) {
|
|
|
|
|
wrapperRef.current.removeEventListener("wheel", handleWheel);
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-03-01 21:55:27 +02:00
|
|
|
}, [wrapperRef.current, scrollY, ganttHeight, barTasks, rowHeight]);
|
2020-08-29 14:01:04 +03:00
|
|
|
|
|
|
|
|
const handleScrollY = (event: SyntheticEvent<HTMLDivElement>) => {
|
|
|
|
|
if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) {
|
2020-08-25 20:58:36 +03:00
|
|
|
setScrollY(event.currentTarget.scrollTop);
|
2020-08-29 14:01:04 +03:00
|
|
|
}
|
|
|
|
|
setIgnoreScrollEvent(false);
|
2020-08-25 20:58:36 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleScrollX = (event: SyntheticEvent<HTMLDivElement>) => {
|
2020-09-09 17:08:16 +03:00
|
|
|
if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) {
|
2020-08-25 20:58:36 +03:00
|
|
|
setScrollX(event.currentTarget.scrollLeft);
|
2020-09-09 17:08:16 +03:00
|
|
|
}
|
|
|
|
|
setIgnoreScrollEvent(false);
|
2020-08-24 23:47:22 +03:00
|
|
|
};
|
|
|
|
|
|
2020-08-29 14:01:04 +03:00
|
|
|
/**
|
|
|
|
|
* Handles arrow keys events and transform it to new scroll
|
|
|
|
|
*/
|
2020-08-24 23:47:22 +03:00
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
2020-08-25 20:58:36 +03:00
|
|
|
event.preventDefault();
|
|
|
|
|
let newScrollY = scrollY;
|
|
|
|
|
let newScrollX = scrollX;
|
2020-08-24 23:47:22 +03:00
|
|
|
let isX = true;
|
|
|
|
|
switch (event.key) {
|
|
|
|
|
case "Down": // IE/Edge specific value
|
|
|
|
|
case "ArrowDown":
|
2020-08-25 20:58:36 +03:00
|
|
|
newScrollY += rowHeight;
|
2020-08-24 23:47:22 +03:00
|
|
|
isX = false;
|
|
|
|
|
break;
|
|
|
|
|
case "Up": // IE/Edge specific value
|
|
|
|
|
case "ArrowUp":
|
2020-08-25 20:58:36 +03:00
|
|
|
newScrollY -= rowHeight;
|
2020-08-24 23:47:22 +03:00
|
|
|
isX = false;
|
|
|
|
|
break;
|
2020-08-25 20:58:36 +03:00
|
|
|
case "Left":
|
2020-08-24 23:47:22 +03:00
|
|
|
case "ArrowLeft":
|
2020-08-25 20:58:36 +03:00
|
|
|
newScrollX -= columnWidth;
|
2020-08-24 23:47:22 +03:00
|
|
|
break;
|
|
|
|
|
case "Right": // IE/Edge specific value
|
|
|
|
|
case "ArrowRight":
|
2020-08-25 20:58:36 +03:00
|
|
|
newScrollX += columnWidth;
|
2020-08-24 23:47:22 +03:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (isX) {
|
2020-08-25 20:58:36 +03:00
|
|
|
if (newScrollX < 0) {
|
2021-03-25 23:06:20 +02:00
|
|
|
newScrollX = 0;
|
|
|
|
|
} else if (newScrollX > svgWidth) {
|
|
|
|
|
newScrollX = svgWidth;
|
2020-08-25 20:58:36 +03:00
|
|
|
}
|
2021-03-25 23:06:20 +02:00
|
|
|
setScrollX(newScrollX);
|
2020-08-24 23:47:22 +03:00
|
|
|
} else {
|
|
|
|
|
if (newScrollY < 0) {
|
2021-03-25 23:06:20 +02:00
|
|
|
newScrollY = 0;
|
2020-08-24 23:47:22 +03:00
|
|
|
} else if (newScrollY > ganttFullHeight - ganttHeight) {
|
2021-03-25 23:06:20 +02:00
|
|
|
newScrollY = ganttFullHeight - ganttHeight;
|
2020-08-24 23:47:22 +03:00
|
|
|
}
|
2021-03-25 23:06:20 +02:00
|
|
|
setScrollY(newScrollY);
|
2020-08-24 23:47:22 +03:00
|
|
|
}
|
2020-08-29 14:01:04 +03:00
|
|
|
setIgnoreScrollEvent(true);
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-15 23:23:09 +03:00
|
|
|
/**
|
|
|
|
|
* Task select event
|
|
|
|
|
*/
|
|
|
|
|
const handleSelectedTask = (taskId: string) => {
|
2021-03-01 21:55:27 +02:00
|
|
|
const newSelectedTask = barTasks.find(t => t.id === taskId);
|
|
|
|
|
const oldSelectedTask = barTasks.find(
|
|
|
|
|
t => !!selectedTask && t.id === selectedTask.id
|
|
|
|
|
);
|
|
|
|
|
if (onSelect) {
|
|
|
|
|
if (oldSelectedTask) {
|
|
|
|
|
onSelect(oldSelectedTask, false);
|
2020-09-15 23:23:09 +03:00
|
|
|
}
|
2021-03-01 21:55:27 +02:00
|
|
|
if (newSelectedTask) {
|
|
|
|
|
onSelect(newSelectedTask, true);
|
2020-09-15 23:23:09 +03:00
|
|
|
}
|
|
|
|
|
}
|
2021-03-01 21:55:27 +02:00
|
|
|
setSelectedTask(newSelectedTask);
|
2020-09-15 23:23:09 +03:00
|
|
|
};
|
|
|
|
|
|
2020-07-22 20:50:43 +03:00
|
|
|
const gridProps: GridProps = {
|
|
|
|
|
columnWidth,
|
2021-03-25 23:06:20 +02:00
|
|
|
svgWidth,
|
2021-03-01 21:55:27 +02:00
|
|
|
tasks: tasks,
|
2020-07-22 20:50:43 +03:00
|
|
|
rowHeight,
|
2021-03-21 16:58:20 +02:00
|
|
|
dates: dateSetup.dates,
|
2020-07-30 00:01:51 +03:00
|
|
|
todayColor,
|
2020-07-22 20:50:43 +03:00
|
|
|
};
|
|
|
|
|
const calendarProps: CalendarProps = {
|
2021-03-21 16:58:20 +02:00
|
|
|
dateSetup,
|
2020-07-22 20:50:43 +03:00
|
|
|
locale,
|
|
|
|
|
viewMode,
|
|
|
|
|
headerHeight,
|
|
|
|
|
columnWidth,
|
|
|
|
|
fontFamily,
|
|
|
|
|
fontSize,
|
|
|
|
|
};
|
2020-08-23 22:33:25 +03:00
|
|
|
const barProps: TaskGanttContentProps = {
|
2021-03-01 21:55:27 +02:00
|
|
|
tasks: barTasks,
|
2021-03-21 16:58:20 +02:00
|
|
|
dates: dateSetup.dates,
|
2021-03-01 21:55:27 +02:00
|
|
|
ganttEvent,
|
2020-09-15 23:23:09 +03:00
|
|
|
selectedTask,
|
2020-07-22 20:50:43 +03:00
|
|
|
rowHeight,
|
2021-03-01 21:55:27 +02:00
|
|
|
taskHeight,
|
2020-07-22 20:50:43 +03:00
|
|
|
columnWidth,
|
|
|
|
|
arrowColor,
|
2020-08-11 01:16:53 +03:00
|
|
|
timeStep,
|
2020-07-22 20:50:43 +03:00
|
|
|
fontFamily,
|
|
|
|
|
fontSize,
|
|
|
|
|
arrowIndent,
|
2020-08-23 22:33:25 +03:00
|
|
|
svgHeight,
|
2021-03-25 23:06:20 +02:00
|
|
|
svgWidth,
|
2021-03-01 21:55:27 +02:00
|
|
|
setGanttEvent,
|
|
|
|
|
setFailedTask,
|
|
|
|
|
setSelectedTask: handleSelectedTask,
|
2020-07-22 20:50:43 +03:00
|
|
|
onDateChange,
|
|
|
|
|
onProgressChange,
|
|
|
|
|
onDoubleClick,
|
|
|
|
|
onTaskDelete,
|
2020-08-23 22:33:25 +03:00
|
|
|
TooltipContent,
|
2020-07-22 20:50:43 +03:00
|
|
|
};
|
2020-08-23 22:33:25 +03:00
|
|
|
|
|
|
|
|
const tableProps: TaskListProps = {
|
|
|
|
|
rowHeight,
|
|
|
|
|
rowWidth: listCellWidth,
|
|
|
|
|
fontFamily,
|
|
|
|
|
fontSize,
|
2021-03-01 21:55:27 +02:00
|
|
|
tasks: barTasks,
|
2020-08-23 22:33:25 +03:00
|
|
|
locale,
|
|
|
|
|
headerHeight,
|
2020-08-24 23:47:22 +03:00
|
|
|
scrollY,
|
2020-08-23 22:33:25 +03:00
|
|
|
ganttHeight,
|
|
|
|
|
horizontalContainerClass: styles.horizontalContainer,
|
2021-03-01 21:55:27 +02:00
|
|
|
selectedTask,
|
2020-09-16 23:17:49 +03:00
|
|
|
setSelectedTask: handleSelectedTask,
|
2020-08-23 22:33:25 +03:00
|
|
|
TaskListHeader,
|
|
|
|
|
TaskListTable,
|
|
|
|
|
};
|
2020-07-22 20:50:43 +03:00
|
|
|
return (
|
2020-08-24 23:47:22 +03:00
|
|
|
<div
|
|
|
|
|
className={styles.wrapper}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
tabIndex={0}
|
2020-08-29 14:01:04 +03:00
|
|
|
ref={wrapperRef}
|
2020-08-24 23:47:22 +03:00
|
|
|
>
|
2020-08-23 22:33:25 +03:00
|
|
|
{listCellWidth && <TaskList {...tableProps} />}
|
|
|
|
|
<TaskGantt
|
|
|
|
|
gridProps={gridProps}
|
|
|
|
|
calendarProps={calendarProps}
|
|
|
|
|
barProps={barProps}
|
|
|
|
|
ganttHeight={ganttHeight}
|
2020-08-24 23:47:22 +03:00
|
|
|
scrollY={scrollY}
|
2020-08-25 20:58:36 +03:00
|
|
|
scrollX={scrollX}
|
|
|
|
|
onScroll={handleScrollX}
|
2020-08-23 22:33:25 +03:00
|
|
|
/>
|
|
|
|
|
<Scroll
|
2020-08-24 23:47:22 +03:00
|
|
|
ganttFullHeight={ganttFullHeight}
|
2020-08-23 22:33:25 +03:00
|
|
|
ganttHeight={ganttHeight}
|
|
|
|
|
headerHeight={headerHeight}
|
2020-08-24 23:47:22 +03:00
|
|
|
scroll={scrollY}
|
2020-08-29 14:01:04 +03:00
|
|
|
onScroll={handleScrollY}
|
2020-08-23 22:33:25 +03:00
|
|
|
/>
|
|
|
|
|
</div>
|
2020-07-22 20:50:43 +03:00
|
|
|
);
|
|
|
|
|
};
|