Features
Gantt Chart project management application with support for task dependencies (links), task groups, drag and drop, inline task creating
Data persistence using Spring Data/JPA (the demo uses in-memory H2 database, can be switched to MySQL database)
HTML5 frontend
Lightweight REST/JSON backend implemented as Spring RESTful web service
Uses a trial version of DayPilot Pro for JavaScript (see License below) for the frontend
Spring Boot 2.4
Java 8+
Maven for handling project dependencies
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.
Gantt Chart Initialization
In order to add a JavaScript Gantt chart to our Spring Boot application we need to add the following code to the HTML5 view:
<!-- Gantt chart control placeholder -->
<div id="dp"></div>
<!-- DayPilot library -->
<script src="js/daypilot/daypilot-all.min.js"></script>
<!-- Gantt chart initialization -->
<script>
var dp = new DayPilot.Gantt("dp");
dp.startDate = "2021-10-01";
dp.days = 90;
dp.rowCreateHandling = "Enabled";
dp.init();
</script>
We have already added a couple of Gantt chart configuration properties (startDate
, days
and rowCreateHandling
) that customize the appearance and behavior. For a list of available Gantt chart properties see the API documentation.
This basic code displays an empty Gantt chart. In the following steps, we will add custom functionality.
Adding a New Task to the Gantt Chart
We have already included rowCreateHandling property in our Gantt configuration. This property enables inline task creating. The Gantt chart will display a special row at the bottom that lets the users enter a new task name:
As soon as you enter a new task name (and hit <enter>) the Gantt control will fire onRowCreate event handler. We will use the event handler to call a server-side REST endpoint that saves the new task to a database.
dp.onRowCreate = function(args) {
var params = {
text: args.text,
start: dp.startDate,
end: dp.startDate.addDays(1)
};
DayPilot.Http.ajax({
url: '/api/tasks/create',
data: params,
success: function (ajax) {
var data = ajax.data;
dp.tasks.add(data);
dp.message("Task created");
}
});
};
As soon as the AJAX call is complete we will tell the Gantt chart to display the new task:
success: function (ajax) {
var data = ajax.data;
dp.tasks.add(data);
dp.message("Task created");
}
The new task will be created at the default position. It will have a duration of 1 day:
var params = {
text: args.text,
start: dp.startDate,
end: dp.startDate.addDays(1)
};
The JSON/REST endpoint is implemented using a Spring REST controller:
package org.daypilot.demo.html5ganttchartspring.controller;
// ...
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
TaskRepository tr;
@PostMapping("/create")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Task createTask(@RequestBody TaskCreateParams params) {
Integer max = tr.findMaxOrdinal();
if (max == null) {
max = 1;
}
else {
max += 1;
}
Task e = new Task();
e.setStart(params.start);
e.setEnd(params.end);
e.setText(params.text);
e.setOrdinal(max);
e.setPriority(now());
tr.save(e);
return e;
}
// ...
public static class TaskCreateParams {
public LocalDateTime start;
public LocalDateTime end;
public String text;
}
}
Moving a Task in Time using Drag and Drop
You can move the task to the target date using drag and drop. The drag and drop moving is enabled by default (taskMoveHandling property is set to "Update"). The Gantt chart control fires onTaskMove event when you drop the task at the new position. We will use this event handler to update the task using an AJAX call:
dp.onTaskMove = function(args) {
var params = {
id: args.task.id(),
start: args.newStart,
end: args.newEnd
};
DayPilot.Http.ajax({
url: '/api/tasks/move',
data: params,
success: function (ajax) {
dp.message("Task moved");
}
});
};
Our Spring controller includes a moveTask()
method that updates the task in the database:
package org.daypilot.demo.html5ganttchartspring.controller;
// ...
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
TaskRepository tr;
@PostMapping("/move")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Task moveTask(@RequestBody EventMoveParams params) {
Task e = tr.findById(params.id).get();
e.setStart(params.start);
e.setEnd(params.end);
tr.save(e);
return e;
}
// ...
public static class EventMoveParams {
public Long id;
public LocalDateTime start;
public LocalDateTime end;
}
}
Task Dependencies (Links)
Start dragging the circle at the start or end of a task to create a new link:
The Gantt chart enables drag and drop link creating by default (linkCreateHandling property is set to "Update"
). The control fires onLinkCreate event handler when the drag and drop operations is complete. As in previous event handlers, we will make an AJAX call to notify the server about the action:
dp.onLinkCreate = function(args) {
var params = {
from: args.from,
to: args.to,
type: args.type
};
DayPilot.Http.ajax({
url: '/api/links/create',
data: params,
success: function (ajax) {
var data = ajax.data;
dp.links.add(data);
dp.message("Link created");
}
});
};
The server-side controller (TaskController.java
) creates a new link record in the database:
package org.daypilot.demo.html5ganttchartspring.controller;
// ...
@RestController
@RequestMapping("/api/links")
public class LinkController {
@Autowired
LinkRepository lr;
@Autowired
TaskRepository tr;
@PostMapping("/create")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Link createLink(@RequestBody LinkCreateParams params) {
Task from = tr.findById(params.from).get();
Task to = tr.findById(params.to).get();
Link link = new Link();
link.setFrom(from);
link.setTo(to);
link.setType(params.type);
lr.save(link);
return link;
}
// ...
public static class LinkCreateParams {
public Long from;
public Long to;
public String type;
}
}
Both tasks are linked visually now:
Creating a Task Group
The Gantt chart supports creating task groups. A group can contain one or more tasks that will be displayed as its children. Every task that has children will be automatically converted to a group.
Users can move tasks in the hierarchy using drag and drop. When hovering the mouse cursor over the task name, a drag and drop handle appears:
You can use it to drag the task to a new position:
You can convert a task to a child task by dragging it over the new parent:
The parent task will become a task group:
Row moving is also enabled by default (rowMoveHandling
is set to "Update"
). The Gantt chart fires onRowMove event handler for all changes:
dp.onRowMove = function(args) {
var params = {
source: args.source.id(),
target: args.target.id(),
position: args.position
};
DayPilot.Http.ajax({
url: '/api/tasks/setPosition',
data: params,
success: function (ajax) {
dp.message("Task moved");
},
});
};
The Spring controller includes setTaskPosition()
method that saves the changes:
package org.daypilot.demo.html5ganttchartspring.controller;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.daypilot.demo.html5ganttchartspring.domain.Link;
import org.daypilot.demo.html5ganttchartspring.domain.Task;
import org.daypilot.demo.html5ganttchartspring.domain.usertype.TaskType;
import org.daypilot.demo.html5ganttchartspring.repository.LinkRepository;
import org.daypilot.demo.html5ganttchartspring.repository.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.method.P;
import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
TaskRepository tr;
@PostMapping("/setPosition")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
SetTaskPositionResponse setTaskPosition(@RequestBody SetTaskPositionParams params) {
Task source = tr.findById(params.source).get();
Task target = tr.findById(params.target).get();
int max = 100000;
Task parent = null;
Integer ordinal = null;
Task sourceParent = source.getParent();
switch (params.position) {
case "before":
parent = target.getParent();
ordinal = target.getOrdinal();
break;
case "after":
parent = target.getParent();
ordinal = target.getOrdinal() + 1;
break;
case "child":
parent = target;
ordinal = max;
break;
case "forbidden":
return new SetTaskPositionResponse() {{
message = "Forbidden";
}};
}
source.setParent(parent);
source.setOrdinal(ordinal);
source.setPriority(now());
tr.save(source);
compactOrdinals(sourceParent);
if (sourceParent != parent) {
compactOrdinals(parent);
}
return new SetTaskPositionResponse() {{
message = "Updated";
}};
}
private void compactOrdinals(Task parent) {
if (parent == null) {
return;
}
List<Task> children = tr.findByParentOrderByOrdinalAscPriorityDesc(parent);
if (children.size() > 0) {
parent.setType(TaskType.GROUP);
tr.save(parent);
}
else if (parent.getType() == TaskType.GROUP){
parent.setType(TaskType.TASK);
tr.save(parent);
}
LocalDateTime now = now();
int i = 0;
for (Task task : children) {
task.setOrdinal(i);
task.setPriority(now);
tr.save(task);
i += 1;
}
}
private LocalDateTime now() {
ZoneId UTC = ZoneId.of("UTC");
return ZonedDateTime.now(UTC).toLocalDateTime();
}
// ...
public static class SetTaskPositionParams {
public Long source;
public Long target;
public String position;
}
public static class SetTaskPositionResponse {
public String message;
}
}
Editing a Task
We will add a context menu to the row header of the Gantt control to let the users access additional actions (task editing, deleting). The context menu can be activated using an active area (a hover icon that appears in the upper-right corner of the header):
dp.onBeforeRowHeaderRender = function(args) {
// ...
args.row.columns[0].areas = [
{
right: 3,
top: 3,
width: 16,
height: 16,
style: "cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);",
action: "ContextMenu",
menu: taskMenu,
v: "Hover"
}
];
};
Context menu definition:
var taskMenu = new DayPilot.Menu({
items: [
{
text: "Edit...",
onClick: function(args) {
var task = args.source;
editTask(task);
}
},
{
text: "Delete",
onClick: function(args) {
var task = args.source;
var params = {
id: task.id()
};
DayPilot.Http.ajax({
url: '/api/tasks/delete',
data: params,
success: function (ajax) {
dp.tasks.remove(task);
dp.message("Task deleted");
}
});
}
}
]
});
The "Edit..." context menu item opens a modal dialog with task details:
function editTask(task) {
var completeValues = [
{name: "0%", id: 0},
{name: "50%", id: 50},
{name: "100%", id: 100},
];
var form = [
{name: "Name", id: "text"},
{name: "Type", id: "type", type: "radio", options: [
{
name: "Task", id: "Task", children: [
{name: "Start", id: "start", dateFormat: "MMMM d, yyyy"},
{name: "End", id: "end", dateFormat: "MMMM d, yyyy"},
{name: "Complete", id: "complete", type: "select", options: completeValues},
]
},
{
name: "Milestone", id: "Milestone", children: [
{name: "Start", id: "start", dateFormat: "MMMM d, yyyy"}
]
}
]}
];
DayPilot.Modal.form(form, task.data).then(function(modal) {
if (modal.canceled) {
return;
}
DayPilot.Http.ajax({
url: "/api/tasks/update",
data: modal.result,
success: function(ajax) {
dp.tasks.update(modal.result);
dp.message("Updated");
}
});
})
}
The modal dialog is implemented using DayPilot.Modal.from() from DayPilot Modal. It lets you create the form content dynamically in code. You can pass a custom data object that will be mapped to the modal dialog fields.
When the changes are submitted using the "OK" button it updates the task by calling /api/tasks/update
REST endpoint:
package org.daypilot.demo.html5ganttchartspring.controller;
// ...
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
TaskRepository tr;
@PostMapping("/update")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Task updateTask(@RequestBody TaskUpdateParams params) {
Task task = tr.findById(params.id).get();
if (params.complete != null) {
task.setComplete(params.complete);
}
if (params.start != null) {
task.setStart(params.start);
}
if (params.end != null) {
task.setEnd(params.end);
}
if (params.text != null) {
task.setText(params.text);
}
if (params.type != null) {
if (params.type.equalsIgnoreCase("milestone")) {
task.setType(TaskType.MILESTONE);
}
else {
task.setType(TaskType.TASK);
}
}
tr.save(task);
return task;
}
// ...
public static class TaskUpdateParams {
public Long id;
public LocalDateTime start;
public LocalDateTime end;
public Integer complete;
public String text;
public String type;
}
}
Creating a Milestone
Create a new task, than open an edit dialog and convert it to a milestone:
The milestone will be displayed in the Gantt chart:
Gantt Chart Columns with Custom Data
We want our Gantt chart to display the task duration in an additional column. First, we need to define the columns:
dp.columns = [
{ title: "Name", property: "text", width: 100},
{ title: "Duration", width: 100}
];
Then we can use onBeforeRowHeaderRender to customize the column content for every task/row:
dp.onBeforeRowHeaderRender = function(args) {
args.row.columns[1].html = new DayPilot.Duration(args.task.end().getTime() - args.task.start().getTime()).toString("d") + " days";
// ...
};
Spring Data Persistence
The JPA persistence classes can be found in org.daypilot.demo.html5ganttchartspring.domain
package.
The Task
class defines a structure of the task table (Task.java
):
package org.daypilot.demo.html5ganttchartspring.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.daypilot.demo.html5ganttchartspring.domain.usertype.TaskType;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String text;
@Column(name = "task_start")
LocalDateTime start;
@Column(name = "task_end")
LocalDateTime end;
@ManyToOne
@JsonIgnore
Task parent;
String color;
@Transient
List<Task> children;
int complete;
int ordinal;
LocalDateTime priority;
@Enumerated(EnumType.STRING)
@Column(name = "task_type")
TaskType type = TaskType.TASK;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public LocalDateTime getStart() {
return start;
}
public void setStart(LocalDateTime start) {
this.start = start;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Task getParent() {
return parent;
}
public void setParent(Task parent) {
this.parent = parent;
}
public List<Task> getChildren() {
return children;
}
public void setChildren(List<Task> children) {
this.children = children;
}
public LocalDateTime getPriority() {
return priority;
}
public void setPriority(LocalDateTime priority) {
this.priority = priority;
}
public int getComplete() {
return complete;
}
public void setComplete(int complete) {
this.complete = complete;
}
public int getOrdinal() {
return ordinal;
}
public void setOrdinal(int ordinal) {
this.ordinal = ordinal;
}
public TaskType getType() {
return type;
}
public void setType(TaskType type) {
this.type = type;
}
}
The links are stored using Link
class (Link.java
):
package org.daypilot.demo.html5ganttchartspring.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.persistence.*;
@Entity
public class Link {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne
@JsonIgnore
Task from;
@ManyToOne
@JsonIgnore
Task to;
@Column(name = "link_type")
String type = "FinishToStart";
@JsonProperty("from")
public Long getFromId() {
return from.getId();
}
@JsonProperty("to")
public Long getToId() {
return to.getId();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Task getFrom() {
return from;
}
public void setFrom(Task from) {
this.from = from;
}
public Task getTo() {
return to;
}
public void setTo(Task to) {
this.to = to;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
History
December 23, 2020: Upgraded to Spring Boot 2.4, DayPilot Pro 2020.4.4820; jQuery dependency removed; using
DayPilot.Modal.form()
for the edit modal dialog.October 2, 2017: Initial release