v2.3.4
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
if (el) {
|
||||
api.exec('scroll-chart', {
|
||||
top: el ? el.scrollTop : 0,
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
if (hasFocus && scrollTaskStore) {
|
||||
const { id } = scrollTaskStore;
|
||||
const node = containerRef.current?.querySelector(
|
||||
`.wx-bar[data-id='${id}']`,
|
||||
);
|
||||
if (node) node.focus();
|
||||
if (node) node.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
if (unsub) unsub();
|
||||
};
|
||||
}, [scrollTaskStore, hasFocus]);
|
||||
}, [scrollTaskStore]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.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={
|
||||
|
||||
@@ -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,9 +187,10 @@ 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;
|
||||
@@ -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"
|
||||
|
||||
101
whatsnew.md
Normal file
101
whatsnew.md
Normal 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
|
||||
Reference in New Issue
Block a user