@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/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
- typeof task.dependencies === 'string' ||
250
- !task.dependencies
251
- ) {
252
- let deps = [];
253
- if (task.dependencies) {
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((task_id) => {
1065
- const dependency = this.get_task(task_id);
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, // from_task
1076
- to_bar, // to_task
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(dep_id => {
1103
- const dep_task = this.get_task(dep_id);
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.includes(task.id)
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
- const parent_bar = this.get_bar(parent_bar_id);
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 =
@@ -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;
@@ -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;
@@ -1,5 +1,6 @@
1
1
  :root {
2
2
  --g-arrow-color: #1f2937;
3
+ --g-arrow-hover-color: #007bff;
3
4
  --g-arrow-critical-color: #f5c044;
4
5
  --g-arrow-invalid-color: #ff7676;
5
6
  --g-bar-color: #fff;