diff --git a/README.md b/README.md index eb0b4c1ac..d7ba52f6f 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ npm start | isDisabled | bool | Disables all action for current task. | | fontSize | string | Specifies the taskbar font size locally. | | project | string | Task project name | +| hideChildren | bool | Hide children items. | \*Required diff --git a/example/package-lock.json b/example/package-lock.json index 7614e022e..4c9c93ecf 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -104,9 +104,9 @@ } }, "@types/node": { - "version": "16.4.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.10.tgz", - "integrity": "sha512-TmVHsm43br64js9BqHWqiDZA+xMtbUpI1MBIA0EyiBmoV9pcEYFOSdj5fr6enZNfh4fChh+AGOLIzGwJnkshyQ==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "@types/testing-library__jest-dom": { "version": "5.14.1", @@ -179,9 +179,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "core-js-pure": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.0.tgz", - "integrity": "sha512-wzlhZNepF/QA9yvx3ePDgNGudU5KDB8lu/TRPKelYA/QtSnkS/cLl2W+TIdEX1FAFcBr0YpY7tPDlcmXJ7AyiQ==" + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.1.tgz", + "integrity": "sha512-TyofCdMzx0KMhi84mVRS8rL1XsRk2SPUNz2azmth53iRN0/08Uim9fdhQTaZTG1LqaXHYVci4RDHka6WrXfnvg==" }, "css": { "version": "3.0.0", @@ -436,9 +436,9 @@ } }, "@types/node": { - "version": "16.4.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.10.tgz", - "integrity": "sha512-TmVHsm43br64js9BqHWqiDZA+xMtbUpI1MBIA0EyiBmoV9pcEYFOSdj5fr6enZNfh4fChh+AGOLIzGwJnkshyQ==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "@types/yargs": { "version": "15.0.14", @@ -534,14 +534,14 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "core-js-pure": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.0.tgz", - "integrity": "sha512-wzlhZNepF/QA9yvx3ePDgNGudU5KDB8lu/TRPKelYA/QtSnkS/cLl2W+TIdEX1FAFcBr0YpY7tPDlcmXJ7AyiQ==" + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.16.1.tgz", + "integrity": "sha512-TyofCdMzx0KMhi84mVRS8rL1XsRk2SPUNz2azmth53iRN0/08Uim9fdhQTaZTG1LqaXHYVci4RDHka6WrXfnvg==" }, "dom-accessibility-api": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", - "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==" + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz", + "integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==" }, "escape-string-regexp": { "version": "1.0.5", @@ -681,9 +681,9 @@ } }, "@types/node": { - "version": "16.4.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.10.tgz", - "integrity": "sha512-TmVHsm43br64js9BqHWqiDZA+xMtbUpI1MBIA0EyiBmoV9pcEYFOSdj5fr6enZNfh4fChh+AGOLIzGwJnkshyQ==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "@types/yargs": { "version": "15.0.14", @@ -828,9 +828,9 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/react": { - "version": "17.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz", - "integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==", + "version": "17.0.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz", + "integrity": "sha512-3kCUiOOlQTwUUvjNFkbBTWMTxdTGybz/PfjCw9JmaRGcEDBQh+nGMg7/E9P2rklhJuYVd25IYLNcvqgSPCPksg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", diff --git a/example/src/App.tsx b/example/src/App.tsx index e2717d636..16d40f9d1 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -16,7 +16,7 @@ const App = () => { columnWidth = 250; } - const onTaskChange = (task: Task) => { + const handleTaskChange = (task: Task) => { console.log("On date change Id:" + task.id); let newTasks = tasks.map(t => (t.id === task.id ? task : t)); if (task.project) { @@ -35,7 +35,7 @@ const App = () => { setTasks(newTasks); }; - const onTaskDelete = (task: Task) => { + const handleTaskDelete = (task: Task) => { const conf = window.confirm("Are you sure about " + task.name + " ?"); if (conf) { setTasks(tasks.filter(t => t.id !== task.id)); @@ -43,19 +43,24 @@ const App = () => { return conf; }; - const onProgressChange = async (task: Task) => { + const handleProgressChange = async (task: Task) => { setTasks(tasks.map(t => (t.id === task.id ? task : t))); console.log("On progress change Id:" + task.id); }; - const onDblClick = (task: Task) => { + const handleDblClick = (task: Task) => { alert("On Double Click event Id:" + task.id); }; - const onSelect = (task: Task, isSelected: boolean) => { + const handleSelect = (task: Task, isSelected: boolean) => { console.log(task.name + " has " + (isSelected ? "selected" : "unselected")); }; + const handleExpanderClick = (task: Task) => { + setTasks(tasks.map(t => (t.id === task.id ? task : t))); + console.log("On expander click Id:" + task.id); + }; + return (
{ @@ -79,11 +85,12 @@ const App = () => { ", "homepage": "https://github.com/MaTeMaTuK/gantt-task-react", diff --git a/src/components/gantt/gantt.tsx b/src/components/gantt/gantt.tsx index e2eaf0671..53cd30e52 100644 --- a/src/components/gantt/gantt.tsx +++ b/src/components/gantt/gantt.tsx @@ -1,5 +1,5 @@ import React, { useState, SyntheticEvent, useRef, useEffect } from "react"; -import { ViewMode, GanttProps } from "../../types/public-types"; +import { ViewMode, GanttProps, Task } from "../../types/public-types"; import { GridProps } from "../grid/grid"; import { ganttDateRange, seedDates } from "../../helpers/date-helper"; import { CalendarProps } from "../calendar/calendar"; @@ -16,6 +16,7 @@ import { GanttEvent } from "../../types/gantt-task-actions"; import { DateSetup } from "../../types/date-setup"; import styles from "./gantt.module.css"; import { HorizontalScroll } from "../other/horizontal-scroll"; +import { removeHiddenTasks } from "../../helpers/other-helper"; export const Gantt: React.FunctionComponent = ({ tasks, @@ -54,6 +55,7 @@ export const Gantt: React.FunctionComponent = ({ onDoubleClick, onDelete, onSelect, + onExpanderClick, }) => { const wrapperRef = useRef(null); const taskListRef = useRef(null); @@ -83,7 +85,8 @@ export const Gantt: React.FunctionComponent = ({ // task change events useEffect(() => { - const [startDate, endDate] = ganttDateRange(tasks, viewMode); + const filteredTasks = removeHiddenTasks(tasks); + const [startDate, endDate] = ganttDateRange(filteredTasks, viewMode); let newDates = seedDates(startDate, endDate, viewMode); if (rtl) { newDates = newDates.reverse(); @@ -94,7 +97,7 @@ export const Gantt: React.FunctionComponent = ({ setDateSetup({ dates: newDates, viewMode }); setBarTasks( convertToBarTasks( - tasks, + filteredTasks, newDates, columnWidth, rowHeight, @@ -322,7 +325,11 @@ export const Gantt: React.FunctionComponent = ({ } setSelectedTask(newSelectedTask); }; - + const handleExpanderClick = (task: Task) => { + if (onExpanderClick && task.hideChildren !== undefined) { + onExpanderClick({ ...task, hideChildren: !task.hideChildren }); + } + }; const gridProps: GridProps = { columnWidth, svgWidth, @@ -380,6 +387,7 @@ export const Gantt: React.FunctionComponent = ({ selectedTask, taskListRef, setSelectedTask: handleSelectedTask, + onExpanderClick: handleExpanderClick, TaskListHeader, TaskListTable, }; diff --git a/src/components/task-list/task-list-table.module.css b/src/components/task-list/task-list-table.module.css index b1a753e6b..7f57268e8 100644 --- a/src/components/task-list/task-list-table.module.css +++ b/src/components/task-list/task-list-table.module.css @@ -20,3 +20,19 @@ overflow: hidden; text-overflow: ellipsis; } +.taskListNameWrapper { + display: flex; +} + +.taskListExpander { + color: rgb(86 86 86); + font-size: 0.6rem; + padding: 0.15rem 0.2rem 0rem 0.2rem; + user-select: none; + cursor: pointer; +} +.taskListEmptyExpander { + font-size: 0.6rem; + padding-left: 1rem; + user-select: none; +} diff --git a/src/components/task-list/task-list-table.tsx b/src/components/task-list/task-list-table.tsx index e2a7229c8..c2203e32e 100644 --- a/src/components/task-list/task-list-table.tsx +++ b/src/components/task-list/task-list-table.tsx @@ -11,7 +11,16 @@ export const TaskListTableDefault: React.FC<{ tasks: Task[]; selectedTaskId: string; setSelectedTask: (taskId: string) => void; -}> = ({ rowHeight, rowWidth, tasks, fontFamily, fontSize, locale }) => { + onExpanderClick: (task: Task) => void; +}> = ({ + rowHeight, + rowWidth, + tasks, + fontFamily, + fontSize, + locale, + onExpanderClick, +}) => { const dateTimeOptions: Intl.DateTimeFormatOptions = { weekday: "short", year: "numeric", @@ -27,6 +36,13 @@ export const TaskListTableDefault: React.FC<{ }} > {tasks.map(t => { + let expanderSymbol = ""; + if (t.hideChildren === false) { + expanderSymbol = "▼"; + } else if (t.hideChildren === true) { + expanderSymbol = "▶"; + } + return (
-  {t.name} +
+
onExpanderClick(t)} + > + {expanderSymbol} +
+
{t.name}
+
void; + onExpanderClick: (task: Task) => void; TaskListHeader: React.FC<{ headerHeight: number; rowWidth: string; @@ -31,6 +32,7 @@ export type TaskListProps = { tasks: Task[]; selectedTaskId: string; setSelectedTask: (taskId: string) => void; + onExpanderClick: (task: Task) => void; }>; }; @@ -44,6 +46,7 @@ export const TaskList: React.FC = ({ tasks, selectedTask, setSelectedTask, + onExpanderClick, locale, ganttHeight, taskListRef, @@ -74,6 +77,7 @@ export const TaskList: React.FC = ({ locale, selectedTaskId: selectedTaskId, setSelectedTask, + onExpanderClick, }; return ( diff --git a/src/helpers/bar-helper.ts b/src/helpers/bar-helper.ts index 9531c0cb0..0dd5b37da 100644 --- a/src/helpers/bar-helper.ts +++ b/src/helpers/bar-helper.ts @@ -63,6 +63,19 @@ export const convertToBarTasks = ( } return task; }); + // normalize flags for hideChildren + barTasks = barTasks.map(task => { + if (task.barChildren.length > 0) { + if (!task.hideChildren) { + task.hideChildren = false; + } + } else if (!task.hideChildren && task.type === "project") { + task.hideChildren = false; + } else if (!task.hideChildren) { + task.hideChildren = undefined; + } + return task; + }); return barTasks; }; diff --git a/src/helpers/other-helper.ts b/src/helpers/other-helper.ts index d45939dae..4718857a4 100644 --- a/src/helpers/other-helper.ts +++ b/src/helpers/other-helper.ts @@ -16,3 +16,33 @@ export function isMouseEvent( export function isBarTask(task: Task | BarTask): task is BarTask { return (task as BarTask).x1 !== undefined; } + +export function removeHiddenTasks(tasks: Task[]) { + const groupedTasks = tasks.filter(t => t.hideChildren); + if (groupedTasks.length > 0) { + for (let i = 0; groupedTasks.length > i; i++) { + const groupedTask = groupedTasks[i]; + const children = getChildren(tasks, groupedTask); + tasks = tasks.filter(t => children.indexOf(t) === -1); + } + } + return tasks; +} + +function getChildren(taskList: Task[], task: Task) { + let tasks: Task[] = []; + if (task.type !== "project") { + tasks = taskList.filter( + t => t.dependencies && t.dependencies.indexOf(task.id) !== -1 + ); + } else { + tasks = taskList.filter(t => t.project && t.project === task.id); + } + const taskChildren = tasks.reduce( + (children: Task[], t) => + children.concat(children, getChildren(taskList, t)), + [] + ); + tasks = tasks.concat(tasks, taskChildren); + return tasks; +} diff --git a/src/helpers/reducer.ts b/src/helpers/reducer.ts deleted file mode 100644 index 6c4811c8c..000000000 --- a/src/helpers/reducer.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/types/public-types.ts b/src/types/public-types.ts index f51b88c1c..b405bb8d6 100644 --- a/src/types/public-types.ts +++ b/src/types/public-types.ts @@ -26,6 +26,7 @@ export interface Task { isDisabled?: boolean; project?: string; dependencies?: string[]; + hideChildren?: boolean; } export interface EventOption { @@ -57,6 +58,10 @@ export interface EventOption { * Invokes on delete selected task. Chart undoes operation if method return false or error. */ onDelete?: (task: Task) => void | boolean | Promise | Promise; + /** + * Invokes on expander on task list + */ + onExpanderClick?: (task: Task) => void; } export interface DisplayOption { @@ -119,6 +124,7 @@ export interface StylingOption { * Sets selected task by id */ setSelectedTask: (taskId: string) => void; + onExpanderClick: (task: Task) => void; }>; }