From cbded1ad8a03d24ec29deba8b4e6c869d26adaaf Mon Sep 17 00:00:00 2001 From: MaTeMaTuK Date: Mon, 1 Mar 2021 21:55:27 +0200 Subject: [PATCH] dev temp changes --- example/package-lock.json | 115 +- example/src/App.tsx | 79 +- example/src/helper.tsx | 77 + package-lock.json | 1372 ++++++++++++----- package.json | 26 +- src/components/gantt/gantt.tsx | 173 ++- src/components/gantt/task-gantt-content.tsx | 193 +-- src/components/other/arrow.tsx | 6 +- src/components/other/tooltip.tsx | 11 +- .../{ => task-item}/bar/bar-date-handle.tsx | 0 .../{ => task-item}/bar/bar-display.tsx | 34 +- .../bar/bar-progress-handle.tsx | 0 src/components/task-item/bar/bar.module.css | 22 + src/components/{ => task-item}/bar/bar.tsx | 60 +- .../task-item/milestone/milestone.module.css | 8 + .../task-item/milestone/milestone.tsx | 37 + src/components/task-item/task-item.tsx | 102 ++ .../task-list.module.css} | 23 - src/components/task-list/task-list-table.tsx | 3 +- src/components/task-list/task-list.tsx | 10 +- src/helpers/bar-helper.ts | 201 ++- src/helpers/date-helper.ts | 2 +- src/helpers/reducer.ts | 26 + src/test/gant.test.tsx | 1 + src/types/gantt-task-actions.ts | 17 + src/types/public-types.ts | 4 + 26 files changed, 1725 insertions(+), 877 deletions(-) create mode 100644 example/src/helper.tsx rename src/components/{ => task-item}/bar/bar-date-handle.tsx (100%) rename src/components/{ => task-item}/bar/bar-display.tsx (62%) rename src/components/{ => task-item}/bar/bar-progress-handle.tsx (100%) create mode 100644 src/components/task-item/bar/bar.module.css rename src/components/{ => task-item}/bar/bar.tsx (59%) create mode 100644 src/components/task-item/milestone/milestone.module.css create mode 100644 src/components/task-item/milestone/milestone.tsx create mode 100644 src/components/task-item/task-item.tsx rename src/components/{bar/bar.module.css => task-item/task-list.module.css} (60%) create mode 100644 src/helpers/reducer.ts create mode 100644 src/types/gantt-task-actions.ts diff --git a/example/package-lock.json b/example/package-lock.json index edf9387..2faacda 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -35,9 +35,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -75,9 +75,9 @@ } }, "@types/yargs": { - "version": "13.0.10", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", - "integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==", + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", "requires": { "@types/yargs-parser": "*" } @@ -301,17 +301,17 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/runtime-corejs3": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", - "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", "requires": { "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" @@ -346,11 +346,6 @@ "wait-for-expect": "^3.0.2" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -379,18 +374,18 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "16.9.49", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz", - "integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==", + "version": "16.9.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.56.tgz", + "integrity": "sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "@types/react-dom": { - "version": "16.9.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", - "integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==", + "version": "16.9.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.9.tgz", + "integrity": "sha512-jE16FNWO3Logq/Lf+yvEAjKzhpST/Eac8EMd1i4dgZdMczfgqC8EjpxwNgEe3SExHYLliabXDh9DEhhqnlXJhg==", "requires": { "@types/react": "*" } @@ -427,9 +422,9 @@ } }, "@types/yargs": { - "version": "13.0.10", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", - "integrity": "sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==", + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", "requires": { "@types/yargs-parser": "*" } @@ -471,11 +466,10 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -508,14 +502,14 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "core-js-pure": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", - "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.7.0.tgz", + "integrity": "sha512-EZD2ckZysv8MMt4J6HSvS9K2GdtlZtdBncKAmF9lr2n0c9dJUaUN88PSTjvgwCgQPWKTkERXITgS6JJRAnljtg==" }, "csstype": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", - "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", + "integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" }, "dom-accessibility-api": { "version": "0.3.0", @@ -550,9 +544,9 @@ } }, "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "version": "15.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", + "integrity": "sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==", "requires": { "@types/yargs-parser": "*" } @@ -563,11 +557,10 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -634,11 +627,6 @@ "chalk": "^3.0.0" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -662,9 +650,9 @@ } }, "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "version": "15.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", + "integrity": "sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==", "requires": { "@types/yargs-parser": "*" } @@ -680,11 +668,10 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -780,9 +767,9 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "csstype": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", - "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", + "integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" } } }, @@ -799,18 +786,18 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "16.9.49", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz", - "integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==", + "version": "16.9.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.56.tgz", + "integrity": "sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "csstype": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", - "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", + "integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" } } }, diff --git a/example/src/App.tsx b/example/src/App.tsx index 63e9a81..732b501 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,11 +2,12 @@ import React from "react"; import "gantt-task-react/dist/index.css"; import { Task, ViewMode, Gantt } from "gantt-task-react"; import { ViewSwitcher } from "./components/view-switcher"; +import { initTasks } from "./helper"; //Init const App = () => { - const currentDate = new Date(); const [view, setView] = React.useState(ViewMode.Day); + const [tasks, setTasks] = React.useState(initTasks()); const [isChecked, setIsChecked] = React.useState(true); let columnWidth = 60; if (view === ViewMode.Month) { @@ -14,85 +15,24 @@ const App = () => { } else if (view === ViewMode.Week) { columnWidth = 250; } - let tasks: Task[] = [ - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1), - end: new Date( - currentDate.getFullYear(), - currentDate.getMonth(), - 2, - 12, - 28 - ), - name: "Idea", - id: "Task 0", - progress: 45, - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0), - name: "Research", - id: "Task 1", - progress: 25, - dependencies: ["Task 0"], - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0), - name: "Discussion with team", - id: "Task 2", - progress: 10, - dependencies: ["Task 1"], - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0), - name: "Developing", - id: "Task 3", - progress: 2, - dependencies: ["Task 2"], - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10), - name: "Review", - id: "Task 4", - progress: 70, - dependencies: ["Task 2"], - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 16), - name: "Release & Eat Pizza", - id: "Task 6", - progress: currentDate.getMonth(), - dependencies: ["Task 4"], - styles: { progressColor: "#ffbb54", progressSelectedColor: "#ff9e0d" }, - }, - { - start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 24), - end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 25), - name: "Closing", - id: "Task 9", - progress: 0, - isDisabled: true, - }, - ]; - const sleep = (milliseconds: number) => { - return new Promise(resolve => setTimeout(resolve, milliseconds)); - }; let onTaskChange = (task: Task) => { console.log("On date change Id:" + task.id); + debugger; + const newTasks = tasks.map(t => (t.id === task.id ? task : t)); + setTasks(newTasks); }; let onTaskDelete = (task: Task) => { const conf = window.confirm("Are you sure about " + task.name + " ?"); + if (conf) { + setTasks(tasks.filter(t => t.id !== task.id)); + } return conf; }; let onProgressChange = async (task: Task) => { - await sleep(5000); + setTasks(tasks.map(t => (t.id === task.id ? task : t))); console.log("On progress change Id:" + task.id); }; @@ -123,6 +63,7 @@ const App = () => { listCellWidth={isChecked ? "155px" : ""} columnWidth={columnWidth} /> +

Milestones are not available

Gantt With Limited Height

= ({ +export const Gantt: React.FunctionComponent = ({ tasks, headerHeight = 50, columnWidth = 60, @@ -27,6 +30,8 @@ export const Gantt: React.SFC = ({ barProgressSelectedColor = "#8282f5", barBackgroundColor = "#b8c2cc", barBackgroundSelectedColor = "#aeb8c2", + milestoneBackgroundColor = "#f1c453", + milestoneBackgroundSelectedColor = "#f29e4c", handleWidth = 8, timeStep = 300000, arrowColor = "grey", @@ -44,21 +49,106 @@ export const Gantt: React.SFC = ({ onSelect, }) => { const wrapperRef = useRef(null); - const [ganttTasks, setGanttTasks] = useState(tasks); - const [selectedTask, setSelectedTask] = useState(""); + const [dates, setDates] = useState(() => { + const [startDate, endDate] = ganttDateRange(tasks, viewMode); + return seedDates(startDate, endDate, viewMode); + }); + + const [taskHeight, setTaskHeight] = useState((rowHeight * barFill) / 100); + const [barTasks, setBarTasks] = useState([]); + + const [ganttEvent, setGanttEvent] = useState({ + action: "", + }); + + const [selectedTask, setSelectedTask] = useState(); + const [failedTask, setFailedTask] = useState(null); const [scrollY, setScrollY] = useState(0); const [scrollX, setScrollX] = useState(0); const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false); - const [startDate, endDate] = ganttDateRange(ganttTasks, viewMode); - const dates = seedDates(startDate, endDate, viewMode); - const svgHeight = rowHeight * ganttTasks.length; + const svgHeight = rowHeight * barTasks.length; const gridWidth = dates.length * columnWidth; - const ganttFullHeight = ganttTasks.length * rowHeight; + const ganttFullHeight = barTasks.length * rowHeight; + + // task change events + useEffect(() => { + const [startDate, endDate] = ganttDateRange(tasks, viewMode); + const newDates = seedDates(startDate, endDate, viewMode); + setDates(newDates); + 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(() => { - setGanttTasks(tasks); - }, [tasks]); + 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]); // scroll events useEffect(() => { @@ -79,7 +169,7 @@ export const Gantt: React.SFC = ({ if ( wrapperRef.current && ganttHeight && - ganttHeight < ganttTasks.length * rowHeight + ganttHeight < barTasks.length * rowHeight ) { wrapperRef.current.addEventListener("wheel", handleWheel, { passive: false, @@ -90,7 +180,7 @@ export const Gantt: React.SFC = ({ wrapperRef.current.removeEventListener("wheel", handleWheel); } }; - }, [wrapperRef.current, scrollY, ganttHeight, ganttTasks, rowHeight]); + }, [wrapperRef.current, scrollY, ganttHeight, barTasks, rowHeight]); const handleScrollY = (event: SyntheticEvent) => { if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) { @@ -154,40 +244,29 @@ export const Gantt: React.SFC = ({ setIgnoreScrollEvent(true); }; - // task change event - const handleTasksChange = (tasks: Task[]) => { - setGanttTasks(tasks); - }; - /** * Task select event */ const handleSelectedTask = (taskId: string) => { - const newSelectedTask = ganttTasks.find(t => t.id === taskId); - if (newSelectedTask) { - if (onSelect) { - const oldSelectedTask = ganttTasks.find(t => t.id === selectedTask); - if (oldSelectedTask) { - onSelect(oldSelectedTask, false); - } + 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); + } + if (newSelectedTask) { onSelect(newSelectedTask, true); } - setSelectedTask(newSelectedTask.id); - } else { - if (onSelect) { - const oldSelectedTask = ganttTasks.find(t => t.id === selectedTask); - if (oldSelectedTask) { - onSelect(oldSelectedTask, false); - } - } - setSelectedTask(""); } + setSelectedTask(newSelectedTask); }; const gridProps: GridProps = { columnWidth, gridWidth, - tasks: ganttTasks, + tasks: tasks, rowHeight, dates, todayColor, @@ -202,26 +281,22 @@ export const Gantt: React.SFC = ({ fontSize, }; const barProps: TaskGanttContentProps = { - tasks: ganttTasks, - selectedTask, - setSelectedTask: handleSelectedTask, - rowHeight, - barCornerRadius, - columnWidth, + tasks: barTasks, dates, - barFill, - barProgressColor, - barProgressSelectedColor, - barBackgroundColor, - barBackgroundSelectedColor, - handleWidth, + ganttEvent, + selectedTask, + rowHeight, + taskHeight, + columnWidth, arrowColor, timeStep, fontFamily, fontSize, arrowIndent, svgHeight, - onTasksChange: handleTasksChange, + setGanttEvent, + setFailedTask, + setSelectedTask: handleSelectedTask, onDateChange, onProgressChange, onDoubleClick, @@ -234,13 +309,13 @@ export const Gantt: React.SFC = ({ rowWidth: listCellWidth, fontFamily, fontSize, - tasks: ganttTasks, + tasks: barTasks, locale, headerHeight, scrollY, ganttHeight, horizontalContainerClass: styles.horizontalContainer, - selectedTaskId: selectedTask, + selectedTask, setSelectedTask: handleSelectedTask, TaskListHeader, TaskListTable, diff --git a/src/components/gantt/task-gantt-content.tsx b/src/components/gantt/task-gantt-content.tsx index 1e0c358..cdbb23a 100644 --- a/src/components/gantt/task-gantt-content.tsx +++ b/src/components/gantt/task-gantt-content.tsx @@ -1,79 +1,60 @@ import React, { useEffect, useState } from "react"; import { Task, EventOption } from "../../types/public-types"; -import { Bar } from "../bar/bar"; import { BarTask } from "../../types/bar-task"; import { Arrow } from "../other/arrow"; -import { - convertToBarTasks, - handleTaskBySVGMouseEvent, - BarMoveAction, -} from "../../helpers/bar-helper"; +import { handleTaskBySVGMouseEvent } from "../../helpers/bar-helper"; import { Tooltip } from "../other/tooltip"; import { isKeyboardEvent } from "../../helpers/other-helper"; +import { TaskItem } from "../task-item/task-item"; +import { + BarMoveAction, + GanttContentMoveAction, + GanttEvent, +} from "../../types/gantt-task-actions"; -export type GanttContentMoveAction = - | "mouseenter" - | "mouseleave" - | "delete" - | "dblclick" - | "select" - | BarMoveAction; -export type BarEvent = { - changedTask?: BarTask; - originalTask?: BarTask; - action: GanttContentMoveAction; -}; export type TaskGanttContentProps = { - tasks: Task[]; + tasks: BarTask[]; dates: Date[]; - selectedTask: string; + ganttEvent: GanttEvent; + selectedTask: BarTask | undefined; rowHeight: number; - barCornerRadius: number; columnWidth: number; - barFill: number; - barProgressColor: string; - barProgressSelectedColor: string; - barBackgroundColor: string; - barBackgroundSelectedColor: string; - handleWidth: number; timeStep: number; svg?: React.RefObject; svgHeight: number; + taskHeight: number; arrowColor: string; arrowIndent: number; fontSize: string; fontFamily: string; + setGanttEvent: (value: GanttEvent) => void; + setFailedTask: (value: BarTask | null) => void; setSelectedTask: (taskId: string) => void; TooltipContent: React.FC<{ task: Task; fontSize: string; fontFamily: string; }>; - onTasksChange: (tasks: Task[]) => void; } & EventOption; export const TaskGanttContent: React.FC = ({ tasks, dates, + ganttEvent, selectedTask, rowHeight, - barCornerRadius, columnWidth, - barFill, - barProgressColor, - barProgressSelectedColor, - barBackgroundColor, - barBackgroundSelectedColor, - handleWidth, timeStep, svg, svgHeight, + taskHeight, arrowColor, arrowIndent, fontFamily, fontSize, + setGanttEvent, + setFailedTask, setSelectedTask, - onTasksChange, onDateChange, onProgressChange, onDoubleClick, @@ -81,11 +62,6 @@ export const TaskGanttContent: React.FC = ({ TooltipContent, }) => { const point = svg?.current?.createSVGPoint(); - const [barEvent, setBarEvent] = useState({ - action: "", - }); - const [barTasks, setBarTasks] = useState([]); - const [failedTask, setFailedTask] = useState(null); const [xStep, setXStep] = useState(0); const [initEventX1Delta, setInitEventX1Delta] = useState(0); const [isMoving, setIsMoving] = useState(false); @@ -101,51 +77,9 @@ export const TaskGanttContent: React.FC = ({ setXStep(newXStep); }, [columnWidth, dates, timeStep]); - // generate tasks - useEffect(() => { - setBarTasks( - convertToBarTasks( - tasks, - dates, - columnWidth, - rowHeight, - barFill, - barCornerRadius, - handleWidth, - barProgressColor, - barProgressSelectedColor, - barBackgroundColor, - barBackgroundSelectedColor - ) - ); - }, [ - tasks, - rowHeight, - barCornerRadius, - columnWidth, - dates, - barFill, - handleWidth, - barProgressColor, - barProgressSelectedColor, - barBackgroundColor, - barBackgroundSelectedColor, - ]); - - // on failed task update - useEffect(() => { - if (failedTask) { - const newTasks = barTasks.map(t => - t.id === failedTask.id ? failedTask : t - ); - onTasksChange(newTasks); - setFailedTask(null); - } - }, [failedTask, barTasks]); - useEffect(() => { const handleMouseMove = async (event: MouseEvent) => { - if (!barEvent.changedTask || !point || !svg?.current) return; + if (!ganttEvent.changedTask || !point || !svg?.current) return; event.preventDefault(); point.x = event.clientX; @@ -155,54 +89,46 @@ export const TaskGanttContent: React.FC = ({ const { isChanged, changedTask } = handleTaskBySVGMouseEvent( cursor.x, - barEvent.action as BarMoveAction, - barEvent.changedTask, + ganttEvent.action as BarMoveAction, + ganttEvent.changedTask, xStep, timeStep, initEventX1Delta ); if (isChanged) { - setBarTasks( - barTasks.map(t => (t.id === changedTask.id ? changedTask : t)) - ); - setBarEvent({ ...barEvent, changedTask: changedTask }); + setGanttEvent({ action: ganttEvent.action, changedTask }); } }; const handleMouseUp = async (event: MouseEvent) => { - const { changedTask: selectedTask, action, originalTask } = barEvent; - - if (!selectedTask || !point || !svg?.current || !originalTask) return; + const { action, originalSelectedTask, changedTask } = ganttEvent; + if (!changedTask || !point || !svg?.current || !originalSelectedTask) + return; event.preventDefault(); point.x = event.clientX; const cursor = point.matrixTransform( svg?.current.getScreenCTM()?.inverse() ); - - const { changedTask } = handleTaskBySVGMouseEvent( + const { changedTask: newChangedTask } = handleTaskBySVGMouseEvent( cursor.x, action as BarMoveAction, - selectedTask, + changedTask, xStep, timeStep, initEventX1Delta ); const isNotLikeOriginal = - originalTask.start !== changedTask.start || - originalTask.end !== changedTask.end || - originalTask.progress !== changedTask.progress; + originalSelectedTask.start !== newChangedTask.start || + originalSelectedTask.end !== newChangedTask.end || + originalSelectedTask.progress !== newChangedTask.progress; // remove listeners svg.current.removeEventListener("mousemove", handleMouseMove); svg.current.removeEventListener("mouseup", handleMouseUp); - setBarEvent({ action: "" }); + setGanttEvent({ action: "" }); setIsMoving(false); - const newTasks = barTasks.map(t => - t.id === changedTask.id ? changedTask : t - ); - onTasksChange(newTasks); // custom operation start let operationSuccess = true; @@ -212,7 +138,7 @@ export const TaskGanttContent: React.FC = ({ isNotLikeOriginal ) { try { - const result = await onDateChange(changedTask); + const result = await onDateChange(newChangedTask); if (result !== undefined) { operationSuccess = result; } @@ -221,7 +147,7 @@ export const TaskGanttContent: React.FC = ({ } } else if (onProgressChange && isNotLikeOriginal) { try { - const result = await onProgressChange(changedTask); + const result = await onProgressChange(newChangedTask); if (result !== undefined) { operationSuccess = result; } @@ -232,16 +158,16 @@ export const TaskGanttContent: React.FC = ({ // If operation is failed - return old state if (!operationSuccess) { - setFailedTask(originalTask); + setFailedTask(originalSelectedTask); } }; if ( !isMoving && - (barEvent.action === "move" || - barEvent.action === "end" || - barEvent.action === "start" || - barEvent.action === "progress") && + (ganttEvent.action === "move" || + ganttEvent.action === "end" || + ganttEvent.action === "start" || + ganttEvent.action === "progress") && svg?.current ) { svg.current.addEventListener("mousemove", handleMouseMove); @@ -249,8 +175,7 @@ export const TaskGanttContent: React.FC = ({ setIsMoving(true); } }, [ - barTasks, - barEvent, + ganttEvent, xStep, initEventX1Delta, onProgressChange, @@ -280,9 +205,7 @@ export const TaskGanttContent: React.FC = ({ try { const result = await onTaskDelete(task); if (result !== undefined && result) { - const newTasks = barTasks.filter(t => t.id !== task.id); - onTasksChange(newTasks); - setSelectedTask(""); + setGanttEvent({ action, changedTask: task }); } } catch (error) { console.error("Error on Delete. " + error); @@ -292,16 +215,16 @@ export const TaskGanttContent: React.FC = ({ } // Mouse Events else if (action === "mouseenter") { - if (!barEvent.action) { - setBarEvent({ + if (!ganttEvent.action) { + setGanttEvent({ action, changedTask: task, - originalTask: task, + originalSelectedTask: task, }); } } else if (action === "mouseleave") { - if (barEvent.action === "mouseenter") { - setBarEvent({ action: "" }); + if (ganttEvent.action === "mouseenter") { + setGanttEvent({ action: "" }); } } else if (action === "dblclick") { !!onDoubleClick && onDoubleClick(task); @@ -314,16 +237,16 @@ export const TaskGanttContent: React.FC = ({ svg.current.getScreenCTM()?.inverse() ); setInitEventX1Delta(cursor.x - task.x1); - setBarEvent({ + setGanttEvent({ action, changedTask: task, - originalTask: task, + originalSelectedTask: task, }); } else { - setBarEvent({ + setGanttEvent({ action, changedTask: task, - originalTask: task, + originalSelectedTask: task, }); } }; @@ -331,14 +254,15 @@ export const TaskGanttContent: React.FC = ({ return ( - {barTasks.map(task => { + {tasks.map(task => { return task.barChildren.map(child => { return ( ); @@ -346,28 +270,29 @@ export const TaskGanttContent: React.FC = ({ })} - {barTasks.map(task => { + {tasks.map(task => { return ( - ); })} - {barEvent.changedTask && ( + {ganttEvent.changedTask && ( = ({ taskFrom, taskTo, rowHeight, + taskHeight, arrowIndent, }) => { const indexCompare = taskFrom.index > taskTo.index ? -1 : 1; - const taskToEndPosition = taskTo.y + taskTo.height / 2; + const taskToEndPosition = taskTo.y + taskHeight / 2; - const path = `M ${taskFrom.x2} ${taskFrom.y + taskFrom.height / 2} + const path = `M ${taskFrom.x2} ${taskFrom.y + taskHeight / 2} h ${arrowIndent} v ${(indexCompare * rowHeight) / 2} H ${taskTo.x1 - arrowIndent} diff --git a/src/components/other/tooltip.tsx b/src/components/other/tooltip.tsx index 14f2bd2..8bb01c5 100644 --- a/src/components/other/tooltip.tsx +++ b/src/components/other/tooltip.tsx @@ -71,10 +71,13 @@ export const StandardTooltipContent: React.FC<{ }-${task.start.getFullYear()} - ${task.end.getDate()}-${ task.end.getMonth() + 1 }-${task.end.getFullYear()}`} -

{`Duration: ${~~( - (task.end.getTime() - task.start.getTime()) / - (1000 * 60 * 60 * 24) - )} day(s)`}

+ {task.end.getTime() - task.start.getTime() !== 0 && ( +

{`Duration: ${~~( + (task.end.getTime() - task.start.getTime()) / + (1000 * 60 * 60 * 24) + )} day(s)`}

+ )} +

{!!task.progress && `Progress: ${task.progress} %`}

diff --git a/src/components/bar/bar-date-handle.tsx b/src/components/task-item/bar/bar-date-handle.tsx similarity index 100% rename from src/components/bar/bar-date-handle.tsx rename to src/components/task-item/bar/bar-date-handle.tsx diff --git a/src/components/bar/bar-display.tsx b/src/components/task-item/bar/bar-display.tsx similarity index 62% rename from src/components/bar/bar-display.tsx rename to src/components/task-item/bar/bar-display.tsx index 891cd41..7db10a5 100644 --- a/src/components/bar/bar-display.tsx +++ b/src/components/task-item/bar/bar-display.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from "react"; +import React from "react"; import style from "./bar.module.css"; type BarDisplayProps = { @@ -9,9 +9,6 @@ type BarDisplayProps = { isSelected: boolean; progressWidth: number; barCornerRadius: number; - text: string; - hasChild: boolean; - arrowIndent: number; styles: { backgroundColor: string; backgroundSelectedColor: string; @@ -28,20 +25,9 @@ export const BarDisplay: React.FC = ({ isSelected, progressWidth, barCornerRadius, - text, - hasChild, - arrowIndent, styles, onMouseDown, }) => { - const textRef = useRef(null); - const [isTextInside, setIsTextInside] = useState(true); - - useEffect(() => { - if (textRef.current) - setIsTextInside(textRef.current.getBBox().width < width); - }, [textRef, width]); - const getProcessColor = () => { return isSelected ? styles.progressSelectedColor : styles.progressColor; }; @@ -50,12 +36,6 @@ export const BarDisplay: React.FC = ({ return isSelected ? styles.backgroundSelectedColor : styles.backgroundColor; }; - const getX = () => { - return isTextInside - ? x + width * 0.5 - : x + width + arrowIndent * +hasChild + arrowIndent * 0.2; - }; - return ( = ({ rx={barCornerRadius} fill={getProcessColor()} /> - - {text} - ); }; diff --git a/src/components/bar/bar-progress-handle.tsx b/src/components/task-item/bar/bar-progress-handle.tsx similarity index 100% rename from src/components/bar/bar-progress-handle.tsx rename to src/components/task-item/bar/bar-progress-handle.tsx diff --git a/src/components/task-item/bar/bar.module.css b/src/components/task-item/bar/bar.module.css new file mode 100644 index 0000000..dfa1962 --- /dev/null +++ b/src/components/task-item/bar/bar.module.css @@ -0,0 +1,22 @@ +.barWrapper { + cursor: pointer; + outline: none; +} + +.barWrapper:hover .barHandle { + visibility: visible; + opacity: 1; +} + +.barHandle { + fill: #ddd; + cursor: ew-resize; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease; +} + +.barBackground { + user-select: none; + stroke-width: 0; +} diff --git a/src/components/bar/bar.tsx b/src/components/task-item/bar/bar.tsx similarity index 59% rename from src/components/bar/bar.tsx rename to src/components/task-item/bar/bar.tsx index 43ddbbf..2796a5c 100644 --- a/src/components/bar/bar.tsx +++ b/src/components/task-item/bar/bar.tsx @@ -1,36 +1,19 @@ import React from "react"; -import { BarTask } from "../../types/bar-task"; import { progressWithByParams, getProgressPoint, -} from "../../helpers/bar-helper"; -import styles from "./bar.module.css"; -import { GanttContentMoveAction } from "../gantt/task-gantt-content"; +} from "../../../helpers/bar-helper"; import { BarDisplay } from "./bar-display"; import { BarDateHandle } from "./bar-date-handle"; import { BarProgressHandle } from "./bar-progress-handle"; +import { TaskItemProps } from "../task-item"; +import styles from "./bar.module.css"; -export type BarProps = { - task: BarTask; - arrowIndent: number; - isProgressChangeable: boolean; - isDateChangeable: boolean; - isDelete: boolean; - isSelected: boolean; - onEventStart: ( - action: GanttContentMoveAction, - selectedTask: BarTask, - event?: React.MouseEvent | React.KeyboardEvent - ) => any; -}; - -export const Bar: React.FC = ({ +export const Bar: React.FC = ({ task, - arrowIndent, isProgressChangeable, isDateChangeable, onEventStart, - isDelete, isSelected, }) => { const progressWidth = progressWithByParams(task.x1, task.x2, task.progress); @@ -39,33 +22,9 @@ export const Bar: React.FC = ({ task.y, task.height ); - + const handleHeight = task.height - 2; return ( - { - switch (e.key) { - case "Delete": { - if (isDelete) onEventStart("delete", task, e); - break; - } - } - e.stopPropagation(); - }} - onMouseEnter={e => { - onEventStart("mouseenter", task, e); - }} - onMouseLeave={e => { - onEventStart("mouseleave", task, e); - }} - onDoubleClick={e => { - onEventStart("dblclick", task, e); - }} - onFocus={() => { - onEventStart("select", task); - }} - > + = ({ height={task.height} progressWidth={progressWidth} barCornerRadius={task.barCornerRadius} - text={task.name} - hasChild={task.barChildren.length > 0} - arrowIndent={arrowIndent} styles={task.styles} isSelected={isSelected} onMouseDown={e => { @@ -90,7 +46,7 @@ export const Bar: React.FC = ({ x={task.x1 + 1} y={task.y + 1} width={task.handleWidth} - height={task.height - 2} + height={handleHeight} barCornerRadius={task.barCornerRadius} onMouseDown={e => { onEventStart("start", task, e); @@ -101,7 +57,7 @@ export const Bar: React.FC = ({ x={task.x2 - task.handleWidth - 1} y={task.y + 1} width={task.handleWidth} - height={task.height - 2} + height={handleHeight} barCornerRadius={task.barCornerRadius} onMouseDown={e => { onEventStart("end", task, e); diff --git a/src/components/task-item/milestone/milestone.module.css b/src/components/task-item/milestone/milestone.module.css new file mode 100644 index 0000000..f8766ba --- /dev/null +++ b/src/components/task-item/milestone/milestone.module.css @@ -0,0 +1,8 @@ +.milestoneWrapper { + cursor: pointer; + outline: none; +} + +.milestoneBackground { + user-select: none; +} diff --git a/src/components/task-item/milestone/milestone.tsx b/src/components/task-item/milestone/milestone.tsx new file mode 100644 index 0000000..a8e3922 --- /dev/null +++ b/src/components/task-item/milestone/milestone.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { TaskItemProps } from "../task-item"; +import styles from "./milestone.module.css"; + +export const Milestone: React.FC = ({ + task, + isDateChangeable, + onEventStart, + isSelected, +}) => { + const transform = `rotate(45 ${task.x1 + task.height * 0.356} + ${task.y + task.height * 0.85})`; + const getBarColor = () => { + return isSelected + ? task.styles.backgroundSelectedColor + : task.styles.backgroundColor; + }; + + return ( + + { + isDateChangeable && onEventStart("move", task, e); + }} + /> + + ); +}; diff --git a/src/components/task-item/task-item.tsx b/src/components/task-item/task-item.tsx new file mode 100644 index 0000000..8d92fdf --- /dev/null +++ b/src/components/task-item/task-item.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useRef, useState } from "react"; +import { BarTask } from "../../types/bar-task"; +import { GanttContentMoveAction } from "../../types/gantt-task-actions"; +import { Bar } from "./bar/bar"; +import { Milestone } from "./milestone/milestone"; +import style from "./task-list.module.css"; + +export type TaskItemProps = { + task: BarTask; + arrowIndent: number; + taskHeight: number; + isProgressChangeable: boolean; + isDateChangeable: boolean; + isDelete: boolean; + isSelected: boolean; + onEventStart: ( + action: GanttContentMoveAction, + selectedTask: BarTask, + event?: React.MouseEvent | React.KeyboardEvent + ) => any; +}; + +export const TaskItem: React.FC = props => { + const { + task, + arrowIndent, + isDelete, + taskHeight, + isSelected, + onEventStart, + } = { + ...props, + }; + const textRef = useRef(null); + const [taskItem, setTaskItem] = useState(
); + const [isTextInside, setIsTextInside] = useState(true); + + useEffect(() => { + switch (task.type) { + case "milestone": + setTaskItem(); + break; + default: + setTaskItem(); + break; + } + }, [task, isSelected]); + + useEffect(() => { + if (textRef.current) { + setIsTextInside(textRef.current.getBBox().width < task.x2 - task.x1); + } + }, [textRef, task]); + + const getX = () => { + const width = task.x2 - task.x1; + const hasChild = task.barChildren.length > 0; + return isTextInside + ? task.x1 + width * 0.5 + : task.x1 + width + arrowIndent * +hasChild + arrowIndent * 0.2; + }; + + return ( + { + switch (e.key) { + case "Delete": { + if (isDelete) onEventStart("delete", task, e); + break; + } + } + e.stopPropagation(); + }} + onMouseEnter={e => { + onEventStart("mouseenter", task, e); + }} + onMouseLeave={e => { + onEventStart("mouseleave", task, e); + }} + onDoubleClick={e => { + onEventStart("dblclick", task, e); + }} + onFocus={() => { + onEventStart("select", task); + }} + > + {taskItem} + + {task.name} + + + ); +}; diff --git a/src/components/bar/bar.module.css b/src/components/task-item/task-list.module.css similarity index 60% rename from src/components/bar/bar.module.css rename to src/components/task-item/task-list.module.css index 8a6414b..2eec420 100644 --- a/src/components/bar/bar.module.css +++ b/src/components/task-item/task-list.module.css @@ -1,21 +1,3 @@ -.barWrapper { - cursor: pointer; - outline: none; -} - -.barWrapper:hover .barHandle { - visibility: visible; - opacity: 1; -} - -.barHandle { - fill: #ddd; - cursor: ew-resize; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease; -} - .barLabel { fill: #fff; text-anchor: middle; @@ -39,8 +21,3 @@ user-select: none; pointer-events: none; } - -.barBackground { - user-select: none; - stroke-width: 0; -} diff --git a/src/components/task-list/task-list-table.tsx b/src/components/task-list/task-list-table.tsx index 3a1c9ae..1a276e4 100644 --- a/src/components/task-list/task-list-table.tsx +++ b/src/components/task-list/task-list-table.tsx @@ -59,7 +59,8 @@ export const TaskListTableDefault: React.FC<{ maxWidth: rowWidth, }} > -  {t.end.toLocaleDateString(locale, dateTimeOptions)} +   + {t.end.toLocaleDateString(locale, dateTimeOptions)}
); diff --git a/src/components/task-list/task-list.tsx b/src/components/task-list/task-list.tsx index bbe8d10..cf83417 100644 --- a/src/components/task-list/task-list.tsx +++ b/src/components/task-list/task-list.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef } from "react"; +import { BarTask } from "../../types/bar-task"; import { Task } from "../../types/public-types"; export type TaskListProps = { @@ -12,8 +13,8 @@ export type TaskListProps = { locale: string; tasks: Task[]; horizontalContainerClass?: string; - selectedTaskId: string; - setSelectedTask: (taskId: string) => void; + selectedTask: BarTask | undefined; + setSelectedTask: (task: string) => void; TaskListHeader: React.FC<{ headerHeight: number; rowWidth: string; @@ -40,7 +41,7 @@ export const TaskList: React.FC = ({ rowHeight, scrollY, tasks, - selectedTaskId, + selectedTask, setSelectedTask, locale, ganttHeight, @@ -61,6 +62,7 @@ export const TaskList: React.FC = ({ fontSize, rowWidth, }; + const selectedTaskId = selectedTask ? selectedTask.id : ""; const tableProps = { rowHeight, rowWidth, @@ -68,7 +70,7 @@ export const TaskList: React.FC = ({ fontSize, tasks, locale, - selectedTaskId, + selectedTaskId: selectedTaskId, setSelectedTask, }; diff --git a/src/helpers/bar-helper.ts b/src/helpers/bar-helper.ts index a86adc9..9d5a1e4 100644 --- a/src/helpers/bar-helper.ts +++ b/src/helpers/bar-helper.ts @@ -1,26 +1,27 @@ import { Task } from "../types/public-types"; import { BarTask } from "../types/bar-task"; +import { BarMoveAction } from "../types/gantt-task-actions"; export const convertToBarTasks = ( tasks: Task[], dates: Date[], columnWidth: number, rowHeight: number, - barFill: number, + taskHeight: number, barCornerRadius: number, handleWidth: number, barProgressColor: string, barProgressSelectedColor: string, barBackgroundColor: string, - barBackgroundSelectedColor: string + barBackgroundSelectedColor: string, + milestoneBackgroundColor: string, + milestoneBackgroundSelectedColor: string ) => { const dateDelta = dates[1].getTime() - dates[0].getTime() - dates[1].getTimezoneOffset() * 60 * 1000 + dates[0].getTimezoneOffset() * 60 * 1000; - const taskHeight = (rowHeight * barFill) / 100; - let barTasks = tasks.map((t, i) => { return convertToBarTask( t, @@ -35,7 +36,9 @@ export const convertToBarTasks = ( barProgressColor, barProgressSelectedColor, barBackgroundColor, - barBackgroundSelectedColor + barBackgroundSelectedColor, + milestoneBackgroundColor, + milestoneBackgroundSelectedColor ); }); @@ -54,7 +57,62 @@ export const convertToBarTasks = ( return barTasks; }; -export const convertToBarTask = ( +const convertToBarTask = ( + task: Task, + index: number, + dates: Date[], + dateDelta: number, + columnWidth: number, + rowHeight: number, + taskHeight: number, + barCornerRadius: number, + handleWidth: number, + barProgressColor: string, + barProgressSelectedColor: string, + barBackgroundColor: string, + barBackgroundSelectedColor: string, + milestoneBackgroundColor: string, + milestoneBackgroundSelectedColor: string +): BarTask => { + let barTask: BarTask; + switch (task.type) { + case "milestone": + barTask = convertToMilestone( + task, + index, + dates, + dateDelta, + columnWidth, + rowHeight, + taskHeight, + barCornerRadius, + handleWidth, + milestoneBackgroundColor, + milestoneBackgroundSelectedColor + ); + break; + default: + barTask = convertToBar( + task, + index, + dates, + dateDelta, + columnWidth, + rowHeight, + taskHeight, + barCornerRadius, + handleWidth, + barProgressColor, + barProgressSelectedColor, + barBackgroundColor, + barBackgroundSelectedColor + ); + break; + } + return barTask; +}; + +const convertToBar = ( task: Task, index: number, dates: Date[], @@ -94,7 +152,50 @@ export const convertToBarTask = ( }; }; -export const taskXCoordinate = ( +const convertToMilestone = ( + task: Task, + index: number, + dates: Date[], + dateDelta: number, + columnWidth: number, + rowHeight: number, + taskHeight: number, + barCornerRadius: number, + handleWidth: number, + milestoneBackgroundColor: string, + milestoneBackgroundSelectedColor: string +) => { + const x = taskXCoordinate(task.start, dates, dateDelta, columnWidth); + const y = taskYCoordinate(index, rowHeight, taskHeight); + + const x1 = x - taskHeight * 0.5; + const x2 = x + taskHeight * 0.5; + + const rotatedHeight = taskHeight / 1.414; + const styles = { + backgroundColor: milestoneBackgroundColor, + backgroundSelectedColor: milestoneBackgroundSelectedColor, + progressColor: "", + progressSelectedColor: "", + ...task.styles, + }; + return { + ...task, + end: task.start, + x1, + x2, + y, + index, + barCornerRadius, + handleWidth, + progress: 0, + height: rotatedHeight, + barChildren: [], + styles, + }; +}; + +const taskXCoordinate = ( xDate: Date, dates: Date[], dateDelta: number, @@ -119,7 +220,7 @@ export const taskXCoordinate = ( return x; }; -export const taskYCoordinate = ( +const taskYCoordinate = ( index: number, rowHeight: number, taskHeight: number @@ -149,7 +250,7 @@ export const progressByProgressWidth = ( } }; -export const progressByX = (x: number, task: BarTask) => { +const progressByX = (x: number, task: BarTask) => { if (x >= task.x2) return 100; else if (x <= task.x1) return 0; else { @@ -175,7 +276,7 @@ export const getProgressPoint = ( return point.join(","); }; -export const startByX = (x: number, xStep: number, task: BarTask) => { +const startByX = (x: number, xStep: number, task: BarTask) => { if (x >= task.x2 - task.handleWidth * 2) { x = task.x2 - task.handleWidth * 2; } @@ -185,7 +286,7 @@ export const startByX = (x: number, xStep: number, task: BarTask) => { return newX; }; -export const endByX = (x: number, xStep: number, task: BarTask) => { +const endByX = (x: number, xStep: number, task: BarTask) => { if (x <= task.x1 + task.handleWidth * 2) { x = task.x1 + task.handleWidth * 2; } @@ -195,7 +296,7 @@ export const endByX = (x: number, xStep: number, task: BarTask) => { return newX; }; -export const moveByX = (x: number, xStep: number, task: BarTask) => { +const moveByX = (x: number, xStep: number, task: BarTask) => { const steps = Math.round((x - task.x1) / xStep); const additionalXValue = steps * xStep; const newX1 = task.x1 + additionalXValue; @@ -203,7 +304,7 @@ export const moveByX = (x: number, xStep: number, task: BarTask) => { return [newX1, newX2]; }; -export const dateByX = ( +const dateByX = ( x: number, taskX: number, taskDate: Date, @@ -218,8 +319,6 @@ export const dateByX = ( return newDate; }; -export type BarMoveAction = "progress" | "end" | "start" | "move" | ""; - /** * Method handles event in real time(mousemove) and on finish(mouseup) */ @@ -230,7 +329,41 @@ export const handleTaskBySVGMouseEvent = ( xStep: number, timeStep: number, initEventX1Delta: number -) => { +): { isChanged: boolean; changedTask: BarTask } => { + let result: { isChanged: boolean; changedTask: BarTask }; + switch (selectedTask.type) { + case "milestone": + result = handleTaskBySVGMouseEventForMilestone( + svgX, + action, + selectedTask, + xStep, + timeStep, + initEventX1Delta + ); + break; + default: + result = handleTaskBySVGMouseEventForBar( + svgX, + action, + selectedTask, + xStep, + timeStep, + initEventX1Delta + ); + break; + } + return result; +}; + +const handleTaskBySVGMouseEventForBar = ( + svgX: number, + action: BarMoveAction, + selectedTask: BarTask, + xStep: number, + timeStep: number, + initEventX1Delta: number +): { isChanged: boolean; changedTask: BarTask } => { const changedTask: BarTask = { ...selectedTask }; let isChanged = false; switch (action) { @@ -298,3 +431,39 @@ export const handleTaskBySVGMouseEvent = ( } return { isChanged, changedTask }; }; + +const handleTaskBySVGMouseEventForMilestone = ( + svgX: number, + action: BarMoveAction, + selectedTask: BarTask, + xStep: number, + timeStep: number, + initEventX1Delta: number +): { isChanged: boolean; changedTask: BarTask } => { + const changedTask: BarTask = { ...selectedTask }; + let isChanged = false; + switch (action) { + case "move": { + const [newMoveX1, newMoveX2] = moveByX( + svgX - initEventX1Delta, + xStep, + selectedTask + ); + isChanged = newMoveX1 !== selectedTask.x1; + if (isChanged) { + changedTask.start = dateByX( + newMoveX1, + selectedTask.x1, + selectedTask.start, + xStep, + timeStep + ); + changedTask.end = changedTask.start; + changedTask.x1 = newMoveX1; + changedTask.x2 = newMoveX2; + } + break; + } + } + return { isChanged, changedTask }; +}; diff --git a/src/helpers/date-helper.ts b/src/helpers/date-helper.ts index 8fbf871..a985d85 100644 --- a/src/helpers/date-helper.ts +++ b/src/helpers/date-helper.ts @@ -55,7 +55,7 @@ export const startOfDate = (date: Date, scale: DateHelperScales) => { export const ganttDateRange = (tasks: Task[], viewMode: ViewMode) => { let newStartDate: Date = tasks[0].start; - let newEndDate: Date = tasks[0].end; + let newEndDate: Date = tasks[0].start; for (const task of tasks) { if (task.start < newStartDate) { newStartDate = task.start; diff --git a/src/helpers/reducer.ts b/src/helpers/reducer.ts new file mode 100644 index 0000000..6c4811c --- /dev/null +++ b/src/helpers/reducer.ts @@ -0,0 +1,26 @@ +export function foo() { + return 1; +} +// import { BarTask } from "../types/bar-task"; +// export type TaskListAction = +// | { type: GanttContentMoveAction; task: BarTask } +// | { type: "update"; tasks: BarTask[] }; + +// export type TaskListState = { +// tasks: BarTask[]; +// changedTask?: BarTask; +// originalTask?: BarTask; +// selectedTask?: BarTask; +// activeAction: GanttContentMoveAction; +// }; + +// export function taskListReducer(state: TaskListState, action: TaskListAction) { +// switch (action.type) { +// case "update": +// return { ...state, tasks: action.tasks }; +// case "select": +// return { ...state, selectedTask: action.task }; +// default: +// return state; +// } +// } diff --git a/src/test/gant.test.tsx b/src/test/gant.test.tsx index c239f06..8c4c4c5 100644 --- a/src/test/gant.test.tsx +++ b/src/test/gant.test.tsx @@ -14,6 +14,7 @@ describe("gantt", () => { name: "Redesign website", id: "Task 0", progress: 45, + type: "task", }, ]} />, diff --git a/src/types/gantt-task-actions.ts b/src/types/gantt-task-actions.ts new file mode 100644 index 0000000..01c2102 --- /dev/null +++ b/src/types/gantt-task-actions.ts @@ -0,0 +1,17 @@ +import { BarTask } from "./bar-task"; + +export type BarMoveAction = "progress" | "end" | "start" | "move"; +export type GanttContentMoveAction = + | "mouseenter" + | "mouseleave" + | "delete" + | "dblclick" + | "select" + | "" + | BarMoveAction; + +export type GanttEvent = { + changedTask?: BarTask; + originalSelectedTask?: BarTask; + action: GanttContentMoveAction; +}; diff --git a/src/types/public-types.ts b/src/types/public-types.ts index 853e898..6971f4b 100644 --- a/src/types/public-types.ts +++ b/src/types/public-types.ts @@ -6,8 +6,10 @@ export enum ViewMode { Week = "Week", Month = "Month", } +export type TaskType = "task" | "milestone"; export interface Task { id: string; + type: TaskType; name: string; start: Date; end: Date; @@ -85,6 +87,8 @@ export interface StylingOption { barProgressSelectedColor?: string; barBackgroundColor?: string; barBackgroundSelectedColor?: string; + milestoneBackgroundColor?: string; + milestoneBackgroundSelectedColor?: string; arrowColor?: string; arrowIndent?: number; todayColor?: string;