This commit is contained in:
Marta Kowalska
2025-11-21 09:58:28 +00:00
parent 934520a631
commit d4429300aa
8 changed files with 186 additions and 98 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<div className="wx-jlbQoHOz wx-gantt" ref={ganttDivRef} onScroll={onScroll}>
@@ -289,8 +285,6 @@ function Layout(props) {
) : null}
<div className="wx-jlbQoHOz wx-content" ref={chartRef}>
<TimeScales highlightTime={highlightTime} />
<Chart
readonly={readonly}
fullWidth={fullWidth}

View File

@@ -1,7 +1,9 @@
.wx-scale.wx-ZkvhDKir {
position: relative;
position: sticky;
top: 0;
background-color: var(--wx-background);
box-shadow: var(--wx-timescale-shadow);
z-index: 1;
z-index: 5;
}
.wx-row.wx-ZkvhDKir,

View File

@@ -8,11 +8,9 @@ function TimeScale(props) {
const api = useContext(storeContext);
const scales = useStore(api, "_scales");
const scrollLeft = useStore(api, "scrollLeft");
const containerStyle = {
width: `${(scales && scales.width) != null ? scales.width : 0}px`,
left: `${-(scrollLeft != null ? scrollLeft : 0)}px`,
};
return (

View File

@@ -18,14 +18,14 @@ function Bars(props) {
const api = useContext(storeContext);
const [rTasksValue, rTasksCounter] = useStoreWithCounter(api,"_tasks");
const [rLinksValue, rLinksCounter] = useStoreWithCounter(api,"_links");
const areaValue = useStore(api,"area");
const scalesValue = useStore(api,"_scales");
const taskTypesValue = useStore(api,"taskTypes");
const baselinesValue = useStore(api,"baselines");
const selectedValue = useStore(api,"_selected");
const scrollTaskStore = useStore(api,"_scrollTask" );
const [rTasksValue, rTasksCounter] = useStoreWithCounter(api, "_tasks");
const [rLinksValue, rLinksCounter] = useStoreWithCounter(api, "_links");
const areaValue = useStore(api, "area");
const scalesValue = useStore(api, "_scales");
const taskTypesValue = useStore(api, "taskTypes");
const baselinesValue = useStore(api, "baselines");
const selectedValue = useStore(api, "_selected");
const scrollTaskStore = useStore(api, "_scrollTask");
const tasks = useMemo(() => {
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 (
<Fragment key={task.id}>
<div
{!task.$skip && <div
className={'wx-GKbcLEGA ' + barClass}
style={taskStyle(task)}
data-tooltip-id={task.id}
@@ -574,6 +569,7 @@ function Bars(props) {
</div>
) : null}
</div>
}
{baselinesValue && !task.$skip_baseline ? (
<div
className={

View File

@@ -13,6 +13,7 @@ import { hotkeys } from '@svar-ui/grid-store';
import storeContext from '../../context';
import { useStore, useStoreWithCounter } from '@svar-ui/lib-react';
import './Chart.css';
import TimeScales from '../TimeScale.jsx';
function Chart(props) {
const {
@@ -37,17 +38,9 @@ function Chart(props) {
const zoom = useStore(api, "zoom");
const [chartHeight, setChartHeight] = useState();
const [scrollLeft, setScrollLeft] = useState();
const [scrollTop, setScrollTop] = useState();
const chartRef = useRef(null);
const extraRows = 1;
useEffect(() => {
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}
>
<TimeScales highlightTime={highlightTime} scales={scales} />
{markers && markers.length ? (
<div
className="wx-mR7v2Xag wx-markers"
@@ -260,15 +256,15 @@ function Chart(props) {
{selected && selected.length
? selected.map((obj, index) =>
obj.$y ? (
<div
key={obj.id}
className="wx-mR7v2Xag wx-selected"
data-id={obj.id}
style={selectStyle[index]}
></div>
) : null,
)
obj.$y ? (
<div
key={obj.id}
className="wx-mR7v2Xag wx-selected"
data-id={obj.id}
style={selectStyle[index]}
></div>
) : null,
)
: null}
<Links />

101
whatsnew.md Normal file
View File

@@ -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