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 1.5
  • 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. Buy a license.

Gantt Chart Initialization

html5-javascript-gantt-chart-spring-boot-java-initialization.png

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 = "2017-10-01";
    dp.days = 31;
    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

html5-javascript-gantt-chart-spring-boot-java-new-task-row.png

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:

html5-javascript-gantt-chart-spring-boot-java-new-task-inline.png

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.toString(),
        end: dp.startDate.addDays(1).toString()
    };

    $.ajax({
        type: 'POST',
        url: '/api/tasks/create',
        data: JSON.stringify(params),
        success: function (data) {
            dp.tasks.add(new DayPilot.Task(data));
            dp.message("Task created");
        },
        contentType: "application/json",
        dataType: 'json'
    });
};

As soon as the AJAX call is complete we will tell the Gantt chart to display the new task:

  success: function (data) {
    dp.tasks.add(new DayPilot.Task(data));
    dp.message("Task created");
  },

html5-javascript-gantt-chart-spring-boot-java-new-task-default.png

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.toString(),
    end: dp.startDate.addDays(1).toString()
  };

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

html5-javascript-gantt-chart-spring-boot-java-drag-time.png

You can move the task to the target date using drag and drop. The drag and drop moving is enabled by default (DayPilot.Gantt.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.toString(),
        end: args.newEnd.toString()
    };

    $.ajax({
        type: 'POST',
        url: '/api/tasks/move',
        data: JSON.stringify(params),
        success: function (data) {
            dp.message("Task moved");
        },
        contentType: "application/json",
        dataType: 'json'
    });

};

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.findOne(params.id);

        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)

html5-javascript-gantt-chart-spring-boot-java-task-link-hover.png

Start dragging the circle at the start or end of a task to create a new link:

html5-javascript-gantt-chart-spring-boot-java-task-link-creating.png

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
    };
    $.ajax({
        type: 'POST',
        url: '/api/links/create',
        data: JSON.stringify(params),
        success: function (data) {
            dp.links.add(new DayPilot.Link(data));
            dp.message("Link created");
        },
        contentType: "application/json",
        dataType: 'json'
    });

    args.preventDefault(); // we need to add the link only when the ajax call is successful

};

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.findOne(params.from);
        Task to = tr.findOne(params.to);

        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:

html5-javascript-gantt-chart-spring-boot-java-task-link-created.png

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:

html5-javascript-gantt-chart-spring-boot-java-task-moving-handle.png

You can use it to drag the task to a new position:

html5-javascript-gantt-chart-spring-boot-java-drag-row.png

You can convert a task to a child task by dragging it over the new parent:

html5-javascript-gantt-chart-spring-boot-java-task-moving.png

The parent task will become a task group:

html5-javascript-gantt-chart-spring-boot-java-parent-group.png

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
    };

    $.ajax({
        type: 'POST',
        url: '/api/tasks/setPosition',
        data: JSON.stringify(params),
        success: function (data) {
            dp.message("Task moved");
        },
        contentType: "application/json",
        dataType: 'json'
    });

};

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.findOne(params.source);
        Task target = tr.findOne(params.target);

        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

html5-javascript-gantt-chart-spring-boot-java-task-context-menu.png

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.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()
                };
                $.ajax({
                    type: 'POST',
                    url: '/api/tasks/delete',
                    data: JSON.stringify(params),
                    success: function (data) {
                        dp.tasks.remove(task);
                        dp.message("Task deleted");
                    },
                    contentType: "application/json",
                    dataType: 'json'
                });
            }
        }
    ]
});

The "Edit..." context menu item opens a modal dialog with task details:

function editTask(task) {
    new DayPilot.Modal({
        onClosed: function(args) {
            dp.tasks.load("/api/tasks");
        }
    }).showUrl("edit.html#" + task.id());

}

html5-javascript-gantt-chart-spring-boot-java-task-editing.png

The modal dialog with task details is implemented as a simple static HTML view (edit.html). 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Edit Task</title>
    <link rel="stylesheet" href="css/main.css" type="text/css">
    <style type="text/css">
        body { padding: 10px; }
    </style>
    <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<h1>Edit Task</h1>

<form id="f">

    <div class="space">
        <div>Name:</div>
        <div>
            <input id="text" name="text" value=""/>
        </div>
    </div>

    <div class="section-milestone">
        <div class="space" class="">
            <div>Milestone:</div>
            <div>
                <input id="milestone" name="milestone" type="checkbox" />
                <label for="milestone">Milestone</label>
            </div>
        </div>
    </div>

    <div class="space">
        <div>Start:</div>
        <div>
            <input id="start" name="start"/> <a href="#" onclick="startPicker.show(); return false;">Change</a>
        </div>
    </div>

    <div class="section-taskonly">

        <div class="space">
            <div>End:</div>
            <div>
                <input id="end" name="end"/> <a href="#" onclick="endPicker.show(); return false;">Change</a>
            </div>
        </div>

        <div class="space">
            <div>Complete:</div>
            <div>
                <select id="complete" name="complete">
                    <option value='0'>0%</option>
                    <option value='50'>50%</option>
                    <option value='100'>100%</option>
                </select>
            </div>
        </div>
    </div>

    <div class="space">
        <input type="submit" value="OK" />
        <a href="#" id="cancel">Cancel</a>
    </div>

</form>


<script src="js/jquery/jquery-3.2.1.min.js"></script>
<script>
    $(document).ready(function() {
        var id = location.hash.substring(1);
        $.ajax({
            type: 'GET',
            url: '/api/tasks/' + id,
            success: function (data) {
                $("#text").val(data.text);
                $("#complete").val(data.complete);
                if (data.type === "Milestone") {
                    $("#milestone").attr("checked", "checked");
                }
                startPicker.setDate(data.start);
                endPicker.setDate(data.end);

                var isparent = data.type === "Group";
                if (isparent) {
                    $(".section-milestone").hide();
                }

                $("#text").focus();
                $("#milestone").change();

            },
            contentType: "application/json",
            dataType: 'json'
        });

        $("#cancel").click(function() {
            parent.DayPilot.ModalStatic.close();
            return false;
        });

        $("#milestone").change(function() {
            var checked = $(this).is(":checked");
            if (checked) {
                $(".section-taskonly").hide();
            }
            else {
                $(".section-taskonly").show();
                parent.DayPilot.ModalStatic.stretch();
            }
        });

        $("#f").submit(function(ev) {
            var f = $("#f");
            var params = {
                id: id,
                start: startPicker.date.toString(),
                end: endPicker.date.toString(),
                complete: $("#complete").val(),
                text: $("#text").val(),
                milestone: $("#milestone").is(":checked")
            };
            console.log("milestone: " + params.milestone);
            $.ajax({
                type: 'POST',
                url: '/api/tasks/update',
                data: JSON.stringify(params),
                success: function (data) {
                    DayPilot.Modal.close(data);
                },
                contentType: "application/json",
                dataType: 'json'
            });

            return false;
        });

    });


    var startPicker =  new DayPilot.DatePicker({
        target: 'start',
        pattern: 'M/d/yyyy',
        onShow: function() {
            parent.DayPilot.ModalStatic.stretch();
        }
    });

    var endPicker =  new DayPilot.DatePicker({
        target: 'end',
        pattern: 'M/d/yyyy',
        onShow: function() {
            parent.DayPilot.ModalStatic.stretch();
        }
    });

</script>
</body>
</html>

It uses an AJAX call to the Spring controller to load task details:

package org.daypilot.demo.html5ganttchartspring.controller;

// ...

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    @Autowired
    TaskRepository tr;

    @GetMapping("/{id}")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    Task task(@PathVariable("id") Long id) {
        return tr.findOne(id);
    }

    // ...

}

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.findOne(params.id);

        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.milestone != null) {
            if (params.milestone) {
                task.setType(TaskType.MILESTONE);
            }
            else if (task.getType() == TaskType.MILESTONE) {
                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 Boolean milestone;
    }

}

Creating a Milestone

html5-javascript-gantt-chart-spring-boot-java-create-milestone.png

Create a new task, than open an edit dialog and convert it to a milestone:

html5-javascript-gantt-chart-spring-boot-java-convert-to-milestone.png

The milestone will be displayed in the Gantt chart:

html5-javascript-gantt-chart-spring-boot-java-milestone-created.png

Gantt Chart Columns with Custom Data

html5-javascript-gantt-chart-spring-boot-java-columns.png

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.AUTO)
    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.AUTO)
    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;
    }
}