diff --git a/package-lock.json b/package-lock.json index 05e497f..b35b7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@svar-ui/react-gantt", - "version": "2.3.3", + "version": "2.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@svar-ui/react-gantt", - "version": "2.3.3", + "version": "2.3.4", "license": "GPL-3.0", "dependencies": { "@svar-ui/gantt-data-provider": "2.3.0", diff --git a/package.json b/package.json index 62180c1..b7da54f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@svar-ui/react-gantt", - "version": "2.3.3", + "version": "2.3.4", "description": "A customizable, high-performance React Gantt chart component for building project timelines.", "productTag": "gantt", "type": "module", @@ -50,7 +50,8 @@ "dist", "types", "package.json", - "dist-full/index.css" + "dist-full/index.css", + "whatsnew.md" ], "peerDependencies": { "react": ">=18", diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 3dab81a..7432e2a 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -7,7 +7,6 @@ import { useContext, } from 'react'; import { hotkeys } from '@svar-ui/grid-store'; -import TimeScales from './TimeScale.jsx'; import Grid from './grid/Grid.jsx'; import Chart from './chart/Chart.jsx'; import Resizer from './Resizer.jsx'; @@ -31,9 +30,7 @@ function Layout(props) { const rScales = useStore(api, "_scales"); const rCellHeight = useStore(api, "cellHeight"); const rColumns = useStore(api, "columns"); - const scrollTop = useStore(api, "scrollTop"); const rScrollTask = useStore(api, "_scrollTask"); - const [compactMode, setCompactMode] = useState(false); let [gridWidth, setGridWidth] = useState(0); const [ganttWidth, setGanttWidth] = useState(undefined); @@ -131,20 +128,13 @@ function Layout(props) { const ganttDivRef = useRef(null); const pseudoRowsRef = useRef(null); - const syncScroll = useCallback(() => { - const el = ganttDivRef.current; - if (el && scrollTop !== el.scrollTop) el.scrollTop = scrollTop; - }, [scrollTop]); - - useEffect(() => { - syncScroll(); - }, [scrollTop, syncScroll]); - const onScroll = useCallback(() => { const el = ganttDivRef.current; - api.exec('scroll-chart', { - top: el ? el.scrollTop : 0, - }); + if (el) { + api.exec('scroll-chart', { + top: el.scrollTop, + }); + } }, [api]); const latest = useRef({ @@ -193,6 +183,7 @@ function Layout(props) { } if (top !== null) { api.exec('scroll-chart', { top: Math.max(top, 0) }); + ganttDivRef.current.scrollTop = Math.max(top, 0); } } }, @@ -200,7 +191,7 @@ function Layout(props) { ); useEffect(() => { - scrollToTask(rScrollTask); + scrollToTask(rScrollTask); }, [rScrollTask]); useEffect(() => { @@ -229,11 +220,14 @@ function Layout(props) { }, [pseudoRowsRef.current]); const layoutRef = useRef(null); + const cleanupRef = useRef(null); + useEffect(() => { + if (cleanupRef.current) return; const node = layoutRef.current; if (!node) return; - const cleanup = hotkeys(node, { + cleanupRef.current = hotkeys(node, { keys: { 'ctrl+c': true, 'ctrl+v': true, @@ -246,9 +240,11 @@ function Layout(props) { }, }); - - return cleanup.destroy; - }, [api]); + return () => { + cleanupRef.current?.destroy(); + cleanupRef.current = null; + }; + }, []); return (
@@ -289,8 +285,6 @@ function Layout(props) { ) : null}
- - { if (!areaValue || !Array.isArray(rTasksValue)) return []; @@ -66,21 +66,16 @@ function Bars(props) { }, [hasFocus, selectedValue]); useEffect(() => { - if (!scrollTaskStore || typeof scrollTaskStore.subscribe !== 'function') + if (!scrollTaskStore) return; - const unsub = scrollTaskStore.subscribe((value) => { - if (hasFocus && value) { - const { id } = value; - const node = containerRef.current?.querySelector( - `.wx-bar[data-id='${id}']`, - ); - if (node) node.focus(); - } - }); - return () => { - if (unsub) unsub(); - }; - }, [scrollTaskStore, hasFocus]); + if (hasFocus && scrollTaskStore) { + const { id } = scrollTaskStore; + const node = containerRef.current?.querySelector( + `.wx-bar[data-id='${id}']`, + ); + if (node) node.focus({ preventScroll: true }); + } + }, [scrollTaskStore]); useEffect(() => { const el = containerRef.current; @@ -391,7 +386,7 @@ function Bars(props) { setLinkFrom(null); } }, [linkFrom]); - + const onClick = useCallback( (e) => { if (ignoreNextClickRef.current) { @@ -496,7 +491,7 @@ function Bars(props) { }} > {tasks.map((task) => { - if (task.$skip) return null; + if (task.$skip && task.$skip_baseline) return null; const barClass = `wx-bar wx-${taskTypeCss(task.type)}` + (touched && taskMove && task.id === taskMove.id ? ' wx-touch' : '') + @@ -518,7 +513,7 @@ function Bars(props) { : ''); return ( -
) : null}
+ } {baselinesValue && !task.$skip_baseline ? (
{ - setScrollLeft(rScrollLeft); - setScrollTop(rScrollTop); - }, [rScrollLeft, rScrollTop]); - - + const extraRows = 1 + (scales?.rows?.length || 0); const selectStyle = useMemo(() => { const t = []; if (selected && selected.length && cellHeight) { @@ -58,10 +51,6 @@ function Chart(props) { return t; }, [selectedCounter, cellHeight]); - useEffect(() => { - dataRequest(); - }, [chartHeight]); - const chartGridHeight = useMemo( () => Math.max(chartHeight || 0, fullHeight), [chartHeight, fullHeight], @@ -70,25 +59,21 @@ function Chart(props) { useEffect(() => { const el = chartRef.current; if (!el) return; - if (typeof scrollTop === 'number') el.scrollTop = scrollTop; - if (typeof scrollLeft === 'number') el.scrollLeft = scrollLeft; - if (typeof scrollTop === 'number' && scrollTop !== el.scrollTop) - setScroll({ top: true }); - if (typeof scrollLeft === 'number' && scrollLeft !== el.scrollLeft) - setScroll({ left: true }); - }, [scrollTop, scrollLeft]); + + if (typeof rScrollTop === 'number') { + el.scrollTop = rScrollTop; + } + }, [rScrollTop]); const onScroll = () => { - const scroll = { left: true, top: true }; + const scroll = { left: true }; setScroll(scroll); - dataRequest(); }; function setScroll(scroll) { const el = chartRef.current; if (!el) return; const pos = {}; - if (scroll.top) pos.top = el.scrollTop; if (scroll.left) pos.left = el.scrollLeft; api.exec('scroll-chart', pos); } @@ -108,6 +93,10 @@ function Chart(props) { }); } + useEffect(() => { + dataRequest(); + }, [chartHeight, rScrollTop, rScrollLeft]); + const showTask = useCallback( (value) => { if (!value) return; @@ -119,14 +108,16 @@ function Chart(props) { if (!el) return; const { clientWidth } = el; const task = api.getTask(id); - if (task.$x + task.$w < (scrollLeft || 0)) { - setScrollLeft(task.$x - (cellWidth || 0)); - } else if (task.$x >= clientWidth + (scrollLeft || 0)) { + if (task.$x + task.$w < el.scrollLeft) { + api.exec('scroll-chart', { left: task.$x - (cellWidth || 0) }); + el.scrollLeft = task.$x - (cellWidth || 0) + } else if (task.$x >= clientWidth + el.scrollLeft) { const width = clientWidth < task.$w ? cellWidth || 0 : task.$w; - setScrollLeft(task.$x - clientWidth + width); + api.exec('scroll-chart', { left: task.$x - clientWidth + width }); + el.scrollLeft = task.$x - clientWidth + width; } }, - [api, scrollLeft, cellWidth], + [api, cellWidth, rScrollLeft], ); useEffect(() => { @@ -165,10 +156,10 @@ function Chart(props) { : null; }, [scales, highlightTime]); - function handleHotkey(ev) { + const handleHotkey = useCallback((ev) => { ev.eventSource = 'chart'; api.exec('hotkey', ev); - } + }, [api]); useEffect(() => { const el = chartRef.current; @@ -182,10 +173,13 @@ function Chart(props) { }; }, [chartRef.current]); + const cleanupRef = useRef(null); + useEffect(() => { const el = chartRef.current; if (!el) return; - const cleanup = hotkeys(el, { + if (cleanupRef.current) return; + cleanupRef.current = hotkeys(el, { keys: { arrowup: true, arrowdown: true, @@ -193,10 +187,11 @@ function Chart(props) { exec: (v) => handleHotkey(v), }); return () => { - if (typeof cleanup === 'function') cleanup(); + cleanupRef.current?.destroy(); + cleanupRef.current = null; }; - }, [chartRef.current]); - + }, []); + useEffect(() => { const node = chartRef.current; if (!node) return; @@ -215,6 +210,7 @@ function Chart(props) { ref={chartRef} onScroll={onScroll} > + {markers && markers.length ? (
- obj.$y ? ( -
- ) : null, - ) + obj.$y ? ( +
+ ) : null, + ) : null} diff --git a/whatsnew.md b/whatsnew.md new file mode 100644 index 0000000..fd0d2d8 --- /dev/null +++ b/whatsnew.md @@ -0,0 +1,101 @@ +## Version 2.3.4 + +### Fixes + +- Performance regression during scrolling and zooming + +## Version 2.3.3 + +### Fixes + +- Fullscreen hotkey property should be optional +- Incorrect handling of themes +- ContextMenu custom class name cannot be set + +## Version 2.3.2 + +### Fixes + +- Tooltips for bars are not visible + +## Version 2.3 + +### New features + +- TypeScript definitions +- Ability to define "hour" duration unit +- Ability to define "minute" length unit +- Ability to create custom scale units +- `autoScale` property to configure start/end scale behaviour +- Standalone sidebar and modal Editor +- Ability to validate fields in the Editor +- Hotkeys for common actions and navigation +- Multi-sorting for table columns via the sort-tasks action +- Headermenu to hide table columns +- Inline editors for table cells +- Ability to expand/collapse table and chart via buttons + +### Breaking changes + +- The package migrated from wx-react-gantt to @svar-ui/react-gantt + +## Version 1.3.1 + +### Fixes + +- Rendering issues + +## Version 1.3 + +- Released under GPLv3 + +## Version 1.1 + +### New features + +- Summary tasks +- Sorting Grid columns +- Batch mode for RestDataProvider to handle mass operations + +### Updates + +- Common minCellwidth and maxCellWidth for zoom levels +- Skipping meaningless actions in Context Menu and Toolbar + +### Fixes + +- Scroll in Grid is not smooth +- Impossible to resize chart bars on the left more than for one cell +- When a branch is removed, only links of the parent task are removed +- Unstable move down behaviour: tasks are inserted in wrong positions +- Resizing columns: horizontal scrollbar does not appear +- Text of tasks is higher than a dragged bar during reordering +- Last task is misplaced after reordering when there are few tasks +- Reordering of tasks with child tasks is broken +- Impossible to define cellWidth if default zoom is enabled +- Data is removed from the task object after the "update-task" operation +- Outdenting does not work for the 3rd-level tasks +- Auto scale is calculated incorrectly if the last task is a milestone +- Parent task is not always opened after adding a new task +- Task start date is not set according to the top-level target task +- Zooming in and out between levels does not work correctly + +## Version 1.0 + +### Initial functionality + +- Fast behavior and clear API +- Configurable Grid columns +- Configurable Chart scales and cell sizes +- Task types: "project", "task", "milestone" and custom +- Baselines +- Holiday and custom markers in the Chart area +- Configurable Editor panel +- Configurable Context Menu and Toolbar +- Tooltips for tasks in the Chart area +- Readonly mode +- Fullscreen mode +- Mouse-wheel zooming in the Chart area +- Responsive behaviour of the Grid area +- Localization of labels and dates +- Ready-made DataProvider to integrate with server