@workiom/frappe-gantt 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -19
- package/dist/frappe-gantt.css +1 -1
- package/dist/frappe-gantt.es.js +761 -630
- package/dist/frappe-gantt.umd.js +46 -21
- package/package.json +1 -1
- package/src/arrow.js +171 -84
- package/src/bar.js +0 -70
- package/src/defaults.js +2 -2
- package/src/dependency_shifting.js +201 -0
- package/src/index.js +115 -151
- package/src/styles/dark.css +5 -0
- package/src/styles/gantt.css +13 -0
- package/src/styles/light.css +1 -0
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import date_utils from './date_utils';
|
|
2
2
|
import { $, createSVG } from './svg_utils';
|
|
3
|
+
import { compute_dependency_shifts } from './dependency_shifting';
|
|
3
4
|
|
|
4
5
|
import Arrow from './arrow';
|
|
5
6
|
import Bar from './bar';
|
|
@@ -244,20 +245,17 @@ export default class Gantt {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
// dependencies
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
deps = task.dependencies
|
|
255
|
-
.split(',')
|
|
256
|
-
.map((d) => d.trim().replaceAll(' ', '_'))
|
|
257
|
-
.filter((d) => d);
|
|
258
|
-
}
|
|
259
|
-
task.dependencies = deps;
|
|
248
|
+
// dependencies — must be an array of { id, type? } objects
|
|
249
|
+
if (typeof task.dependencies === 'string' ||
|
|
250
|
+
(Array.isArray(task.dependencies) && task.dependencies.some((d) => typeof d === 'string'))) {
|
|
251
|
+
console.warn(`[frappe-gantt] Task "${task.id}": dependencies must be an array of {id, type?} objects. String format is no longer supported.`);
|
|
252
|
+
}
|
|
253
|
+
if (!Array.isArray(task.dependencies)) {
|
|
254
|
+
task.dependencies = [];
|
|
260
255
|
}
|
|
256
|
+
task.dependencies = task.dependencies
|
|
257
|
+
.filter((d) => d && typeof d.id === 'string')
|
|
258
|
+
.map((d) => ({ ...d, id: d.id.replaceAll(' ', '_') }));
|
|
261
259
|
|
|
262
260
|
// uids
|
|
263
261
|
if (!task.id) {
|
|
@@ -283,8 +281,8 @@ export default class Gantt {
|
|
|
283
281
|
this.dependency_map = {};
|
|
284
282
|
for (let t of this.tasks) {
|
|
285
283
|
for (let d of t.dependencies) {
|
|
286
|
-
this.dependency_map[d] = this.dependency_map[d] || [];
|
|
287
|
-
this.dependency_map[d].push(t.id);
|
|
284
|
+
this.dependency_map[d.id] = this.dependency_map[d.id] || [];
|
|
285
|
+
this.dependency_map[d.id].push(t.id);
|
|
288
286
|
}
|
|
289
287
|
}
|
|
290
288
|
}
|
|
@@ -297,7 +295,35 @@ export default class Gantt {
|
|
|
297
295
|
update_task(id, new_details) {
|
|
298
296
|
let task = this.tasks.find((t) => t.id === id);
|
|
299
297
|
let bar = this.bars[task._index];
|
|
298
|
+
|
|
299
|
+
// Check if dependencies are being updated
|
|
300
|
+
const dependenciesChanged = new_details.dependencies !== undefined;
|
|
301
|
+
|
|
300
302
|
Object.assign(task, new_details);
|
|
303
|
+
|
|
304
|
+
// If dependencies changed, rebuild arrows
|
|
305
|
+
if (dependenciesChanged) {
|
|
306
|
+
// Normalize to array of {id, type?} objects; non-array values (e.g. null) become []
|
|
307
|
+
if (!Array.isArray(task.dependencies)) {
|
|
308
|
+
task.dependencies = [];
|
|
309
|
+
}
|
|
310
|
+
task.dependencies = task.dependencies
|
|
311
|
+
.filter((d) => d && typeof d.id === 'string')
|
|
312
|
+
.map((d) => ({ ...d, id: d.id.replaceAll(' ', '_') }));
|
|
313
|
+
|
|
314
|
+
// Rebuild dependency map
|
|
315
|
+
this.setup_dependencies();
|
|
316
|
+
|
|
317
|
+
// Clear existing arrows from the DOM
|
|
318
|
+
this.layers.arrow.innerHTML = '';
|
|
319
|
+
|
|
320
|
+
// Recreate all arrows
|
|
321
|
+
this.make_arrows();
|
|
322
|
+
|
|
323
|
+
// Remap arrows on bars
|
|
324
|
+
this.map_arrows_on_bars();
|
|
325
|
+
}
|
|
326
|
+
|
|
301
327
|
bar.refresh();
|
|
302
328
|
}
|
|
303
329
|
|
|
@@ -1061,21 +1087,23 @@ export default class Gantt {
|
|
|
1061
1087
|
for (let task of this.tasks) {
|
|
1062
1088
|
let arrows = [];
|
|
1063
1089
|
arrows = task.dependencies
|
|
1064
|
-
.map((
|
|
1065
|
-
const dependency = this.get_task(
|
|
1090
|
+
.map((dep) => {
|
|
1091
|
+
const dependency = this.get_task(dep.id);
|
|
1066
1092
|
if (!dependency) return;
|
|
1067
1093
|
|
|
1068
|
-
// Skip if either task has no bar (e.g., tasks without dates)
|
|
1069
1094
|
const from_bar = this.bars[dependency._index];
|
|
1070
1095
|
const to_bar = this.bars[task._index];
|
|
1071
1096
|
if (!from_bar || !to_bar) return;
|
|
1072
1097
|
|
|
1098
|
+
const resolved_type = dep.type || this.options.dependencies_type || 'finish-to-start';
|
|
1073
1099
|
const arrow = new Arrow(
|
|
1074
1100
|
this,
|
|
1075
|
-
from_bar,
|
|
1076
|
-
to_bar,
|
|
1101
|
+
from_bar,
|
|
1102
|
+
to_bar,
|
|
1103
|
+
resolved_type,
|
|
1077
1104
|
);
|
|
1078
1105
|
this.layers.arrow.appendChild(arrow.element);
|
|
1106
|
+
this.layers.arrow.appendChild(arrow.hit_element);
|
|
1079
1107
|
return arrow;
|
|
1080
1108
|
})
|
|
1081
1109
|
.filter(Boolean); // filter falsy values
|
|
@@ -1099,8 +1127,8 @@ export default class Gantt {
|
|
|
1099
1127
|
|
|
1100
1128
|
let maxEF = 0;
|
|
1101
1129
|
if (task.dependencies && task.dependencies.length > 0) {
|
|
1102
|
-
task.dependencies.forEach(
|
|
1103
|
-
const dep_task = this.get_task(
|
|
1130
|
+
task.dependencies.forEach(dep => {
|
|
1131
|
+
const dep_task = this.get_task(dep.id);
|
|
1104
1132
|
if (dep_task) {
|
|
1105
1133
|
const dep_values = calculateES(dep_task);
|
|
1106
1134
|
maxEF = Math.max(maxEF, dep_values.ef);
|
|
@@ -1129,7 +1157,7 @@ export default class Gantt {
|
|
|
1129
1157
|
|
|
1130
1158
|
// Find tasks that depend on this task
|
|
1131
1159
|
const dependents = this.tasks.filter(t =>
|
|
1132
|
-
t.dependencies && t.dependencies.
|
|
1160
|
+
t.dependencies && t.dependencies.some(d => d.id === task.id)
|
|
1133
1161
|
);
|
|
1134
1162
|
|
|
1135
1163
|
let minLS = projectDuration;
|
|
@@ -1460,20 +1488,7 @@ export default class Gantt {
|
|
|
1460
1488
|
x_on_start = e.offsetX || e.layerX;
|
|
1461
1489
|
|
|
1462
1490
|
parent_bar_id = bar_wrapper.getAttribute('data-id');
|
|
1463
|
-
|
|
1464
|
-
const dependencies_type = parent_bar.task.dependencies_type || this.options.dependencies_type;
|
|
1465
|
-
|
|
1466
|
-
let ids;
|
|
1467
|
-
// Only move dependencies during drag if dependencies_type is 'fixed' and move_dependencies is true
|
|
1468
|
-
if (this.options.move_dependencies && dependencies_type === 'fixed') {
|
|
1469
|
-
ids = [
|
|
1470
|
-
parent_bar_id,
|
|
1471
|
-
...this.get_all_dependent_tasks(parent_bar_id),
|
|
1472
|
-
];
|
|
1473
|
-
} else {
|
|
1474
|
-
ids = [parent_bar_id];
|
|
1475
|
-
}
|
|
1476
|
-
bars = ids.map((id) => this.get_bar(id));
|
|
1491
|
+
bars = [this.get_bar(parent_bar_id)];
|
|
1477
1492
|
|
|
1478
1493
|
this.bar_being_dragged = false;
|
|
1479
1494
|
pos = x_on_start;
|
|
@@ -1695,22 +1710,71 @@ export default class Gantt {
|
|
|
1695
1710
|
});
|
|
1696
1711
|
});
|
|
1697
1712
|
|
|
1698
|
-
// Update dependent tasks based on dependencies_type
|
|
1699
|
-
// Only update for the parent bar that was actually moved
|
|
1700
|
-
// DISABLED: Allow invalid dependencies instead of auto-updating
|
|
1701
|
-
// const parent_bar = this.get_bar(parent_bar_id);
|
|
1702
|
-
// if (parent_bar && parent_bar.$bar.finaldx) {
|
|
1703
|
-
// const dependent_changes = this.update_dependent_tasks_by_type(parent_bar);
|
|
1704
|
-
// // Add dependent task changes to the list
|
|
1705
|
-
// tasks_changed.push(...dependent_changes);
|
|
1706
|
-
// }
|
|
1707
|
-
|
|
1708
1713
|
// Recalculate critical path if enabled and any bar was moved
|
|
1709
1714
|
if (this.options.critical_path && bars.some(bar => bar.$bar.finaldx)) {
|
|
1710
1715
|
this.calculate_critical_path();
|
|
1711
1716
|
this.update_arrow_critical_path();
|
|
1712
1717
|
}
|
|
1713
1718
|
|
|
1719
|
+
// Apply dependency shifting
|
|
1720
|
+
if (tasks_changed.length > 0 && this.options.dependency_shifting !== 'none') {
|
|
1721
|
+
// Derive shift direction based on mode and interaction type
|
|
1722
|
+
const _mode = this.options.dependency_shifting;
|
|
1723
|
+
let direction;
|
|
1724
|
+
if (is_resizing_left) {
|
|
1725
|
+
// maintain_buffer_downstream: left-resize does nothing
|
|
1726
|
+
direction = _mode === 'maintain_buffer_downstream' ? 'none' : 'upstream';
|
|
1727
|
+
} else if (is_resizing_right) {
|
|
1728
|
+
direction = 'downstream';
|
|
1729
|
+
} else {
|
|
1730
|
+
// drag: maintain_buffer_downstream pushes downstream only;
|
|
1731
|
+
// maintain_buffer_all and consume_buffer propagate both ways
|
|
1732
|
+
direction = _mode === 'maintain_buffer_downstream' ? 'downstream' : 'both';
|
|
1733
|
+
}
|
|
1734
|
+
tasks_changed.forEach(({ task }) => {
|
|
1735
|
+
if (direction === 'none') return;
|
|
1736
|
+
const dragged_bar = bars.find((b) => b.task.id === task.id);
|
|
1737
|
+
if (!dragged_bar || !dragged_bar.$bar.finaldx) return;
|
|
1738
|
+
|
|
1739
|
+
const units_moved = dragged_bar.$bar.finaldx / this.config.column_width;
|
|
1740
|
+
const ms_per_unit =
|
|
1741
|
+
this.config.unit === 'hour' ? 3600000 :
|
|
1742
|
+
this.config.unit === 'day' ? 86400000 :
|
|
1743
|
+
this.config.unit === 'month' ? 30 * 86400000 :
|
|
1744
|
+
this.config.unit === 'year' ? 365 * 86400000 : 86400000;
|
|
1745
|
+
const deltaMs = units_moved * this.config.step * ms_per_unit;
|
|
1746
|
+
|
|
1747
|
+
const shift_map = compute_dependency_shifts(
|
|
1748
|
+
this.tasks,
|
|
1749
|
+
task.id,
|
|
1750
|
+
deltaMs,
|
|
1751
|
+
this.options.dependency_shifting,
|
|
1752
|
+
direction,
|
|
1753
|
+
);
|
|
1754
|
+
|
|
1755
|
+
shift_map.forEach((shiftMs, taskId) => {
|
|
1756
|
+
const affected_bar = this.get_bar(taskId);
|
|
1757
|
+
if (!affected_bar) return;
|
|
1758
|
+
|
|
1759
|
+
const affected_task = affected_bar.task;
|
|
1760
|
+
const new_start = new Date(affected_task._start.getTime() + shiftMs);
|
|
1761
|
+
const new_x =
|
|
1762
|
+
(date_utils.diff(new_start, this.gantt_start, this.config.unit) /
|
|
1763
|
+
this.config.step) *
|
|
1764
|
+
this.config.column_width;
|
|
1765
|
+
|
|
1766
|
+
affected_bar.update_bar_position({ x: new_x });
|
|
1767
|
+
affected_bar.update_arrow_position();
|
|
1768
|
+
|
|
1769
|
+
this.trigger_event('after_date_change', [
|
|
1770
|
+
affected_task,
|
|
1771
|
+
affected_task._start,
|
|
1772
|
+
date_utils.add(affected_task._end, -1, 'second'),
|
|
1773
|
+
]);
|
|
1774
|
+
});
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1714
1778
|
// Trigger on_after_date_change for all tasks that changed
|
|
1715
1779
|
if (tasks_changed.length > 0) {
|
|
1716
1780
|
tasks_changed.forEach(({task, start, end}) => {
|
|
@@ -1718,6 +1782,10 @@ export default class Gantt {
|
|
|
1718
1782
|
});
|
|
1719
1783
|
}
|
|
1720
1784
|
|
|
1785
|
+
// Reset finaldx so subsequent mouseup events (e.g. from scrolling)
|
|
1786
|
+
// don't re-trigger date changes or dependency shifting
|
|
1787
|
+
bars.forEach((bar) => { bar.$bar.finaldx = 0; });
|
|
1788
|
+
|
|
1721
1789
|
// Reset drag flags after handling callbacks
|
|
1722
1790
|
is_dragging = false;
|
|
1723
1791
|
is_resizing_left = false;
|
|
@@ -1913,110 +1981,6 @@ export default class Gantt {
|
|
|
1913
1981
|
return out.filter(Boolean);
|
|
1914
1982
|
}
|
|
1915
1983
|
|
|
1916
|
-
update_dependent_tasks_by_type(parent_bar) {
|
|
1917
|
-
const dependencies_type = parent_bar.task.dependencies_type || this.options.dependencies_type;
|
|
1918
|
-
const changed_tasks = [];
|
|
1919
|
-
|
|
1920
|
-
// Skip if using fixed dependency type (current behavior)
|
|
1921
|
-
if (dependencies_type === 'fixed') return changed_tasks;
|
|
1922
|
-
|
|
1923
|
-
// Get all tasks that depend on this task
|
|
1924
|
-
const dependent_task_ids = this.dependency_map[parent_bar.task.id] || [];
|
|
1925
|
-
|
|
1926
|
-
dependent_task_ids.forEach(dependent_id => {
|
|
1927
|
-
const dependent_bar = this.get_bar(dependent_id);
|
|
1928
|
-
if (!dependent_bar) return;
|
|
1929
|
-
|
|
1930
|
-
const dependent_task = dependent_bar.task;
|
|
1931
|
-
const dep_type = dependent_task.dependencies_type || this.options.dependencies_type;
|
|
1932
|
-
|
|
1933
|
-
// Calculate new dates based on dependency type
|
|
1934
|
-
let new_start, new_end;
|
|
1935
|
-
const task_duration = date_utils.diff(dependent_task._end, dependent_task._start, 'hour');
|
|
1936
|
-
let should_update = false;
|
|
1937
|
-
|
|
1938
|
-
switch(dep_type) {
|
|
1939
|
-
case 'finish-to-start':
|
|
1940
|
-
// Dependent task starts when parent task finishes
|
|
1941
|
-
// Only update if parent ends after dependent currently starts
|
|
1942
|
-
if (parent_bar.task._end > dependent_task._start) {
|
|
1943
|
-
new_start = new Date(parent_bar.task._end);
|
|
1944
|
-
new_end = date_utils.add(new_start, task_duration, 'hour');
|
|
1945
|
-
should_update = true;
|
|
1946
|
-
}
|
|
1947
|
-
break;
|
|
1948
|
-
|
|
1949
|
-
case 'start-to-start':
|
|
1950
|
-
// Dependent task starts when parent task starts
|
|
1951
|
-
// Only update if parent starts after dependent currently starts
|
|
1952
|
-
if (parent_bar.task._start > dependent_task._start) {
|
|
1953
|
-
new_start = new Date(parent_bar.task._start);
|
|
1954
|
-
new_end = date_utils.add(new_start, task_duration, 'hour');
|
|
1955
|
-
should_update = true;
|
|
1956
|
-
}
|
|
1957
|
-
break;
|
|
1958
|
-
|
|
1959
|
-
case 'finish-to-finish':
|
|
1960
|
-
// Dependent task finishes when parent task finishes
|
|
1961
|
-
// Only update if parent ends after dependent currently ends
|
|
1962
|
-
if (parent_bar.task._end > dependent_task._end) {
|
|
1963
|
-
new_end = new Date(parent_bar.task._end);
|
|
1964
|
-
new_start = date_utils.add(new_end, -task_duration, 'hour');
|
|
1965
|
-
should_update = true;
|
|
1966
|
-
}
|
|
1967
|
-
break;
|
|
1968
|
-
|
|
1969
|
-
case 'start-to-finish':
|
|
1970
|
-
// Dependent task finishes when parent task starts
|
|
1971
|
-
// Only update if parent starts after dependent currently ends
|
|
1972
|
-
if (parent_bar.task._start > dependent_task._end) {
|
|
1973
|
-
new_end = new Date(parent_bar.task._start);
|
|
1974
|
-
new_start = date_utils.add(new_end, -task_duration, 'hour');
|
|
1975
|
-
should_update = true;
|
|
1976
|
-
}
|
|
1977
|
-
break;
|
|
1978
|
-
|
|
1979
|
-
default:
|
|
1980
|
-
return;
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
// Only update if constraint requires it
|
|
1984
|
-
if (!should_update) return;
|
|
1985
|
-
|
|
1986
|
-
// Update the dependent task dates
|
|
1987
|
-
dependent_task._start = new_start;
|
|
1988
|
-
dependent_task._end = new_end;
|
|
1989
|
-
|
|
1990
|
-
// Refresh the dependent bar
|
|
1991
|
-
dependent_bar.compute_x();
|
|
1992
|
-
dependent_bar.compute_duration();
|
|
1993
|
-
dependent_bar.update_bar_position({
|
|
1994
|
-
x: dependent_bar.x,
|
|
1995
|
-
width: dependent_bar.width
|
|
1996
|
-
});
|
|
1997
|
-
|
|
1998
|
-
// Trigger date_change event for the dependent task
|
|
1999
|
-
this.trigger_event('date_change', [
|
|
2000
|
-
dependent_task,
|
|
2001
|
-
new_start,
|
|
2002
|
-
date_utils.add(new_end, -1, 'second'),
|
|
2003
|
-
]);
|
|
2004
|
-
|
|
2005
|
-
// Track this changed task
|
|
2006
|
-
changed_tasks.push({
|
|
2007
|
-
task: dependent_task,
|
|
2008
|
-
start: new_start,
|
|
2009
|
-
end: date_utils.add(new_end, -1, 'second')
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
// Recursively update dependents of this task and collect their changes
|
|
2013
|
-
const recursive_changes = this.update_dependent_tasks_by_type(dependent_bar);
|
|
2014
|
-
changed_tasks.push(...recursive_changes);
|
|
2015
|
-
});
|
|
2016
|
-
|
|
2017
|
-
return changed_tasks;
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
1984
|
get_snap_position(dx, ox) {
|
|
2021
1985
|
let unit_length = 1;
|
|
2022
1986
|
const default_snap =
|
package/src/styles/dark.css
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
--g-text-light-dark: #ececec;
|
|
9
9
|
--g-text-color-dark: #f7f7f7;
|
|
10
10
|
--g-progress-color: #8a8aff;
|
|
11
|
+
--g-arrow-hover-color-dark: #60a5fa;
|
|
11
12
|
--g-arrow-critical-color-dark: #f5c044;
|
|
12
13
|
--g-arrow-invalid-color-dark: #ff7676;
|
|
13
14
|
--g-resize-handle-hover-dark: rgba(96, 165, 250, 0.4);
|
|
@@ -39,6 +40,10 @@
|
|
|
39
40
|
stroke: var(--g-arrow-invalid-color-dark);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
& .arrow-hover {
|
|
44
|
+
stroke: var(--g-arrow-hover-color-dark);
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
& .bar {
|
|
43
48
|
fill: var(--g-bar-color-dark);
|
|
44
49
|
stroke: none;
|
package/src/styles/gantt.css
CHANGED
|
@@ -287,6 +287,11 @@
|
|
|
287
287
|
stroke: var(--g-arrow-invalid-color);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
& .arrow-hover {
|
|
291
|
+
stroke: var(--g-arrow-hover-color);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
290
295
|
& .bar-wrapper .bar {
|
|
291
296
|
fill: var(--g-bar-color);
|
|
292
297
|
stroke: var(--g-bar-border);
|
|
@@ -294,6 +299,14 @@
|
|
|
294
299
|
transition: stroke-width 0.3s ease;
|
|
295
300
|
}
|
|
296
301
|
|
|
302
|
+
& .bar-wrapper .bar.bar-arrow-critical {
|
|
303
|
+
outline-color: var(--g-arrow-critical-color);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
& .bar-wrapper .bar.bar-arrow-invalid {
|
|
307
|
+
outline-color: var(--g-arrow-invalid-color);
|
|
308
|
+
}
|
|
309
|
+
|
|
297
310
|
& .bar-progress {
|
|
298
311
|
fill: var(--g-progress-color);
|
|
299
312
|
border-radius: 4px;
|