Our state of the art Gantt chart


Post by avallete »

Hi there !

I am working on a website which is also distributed as an app which have both the classical "brower desktop" and "electron / webapp" version.

I did some tweaks on the "browser" version and I'm now pretty satisfied on how it's looking. What I needed was mainly 3 functionalities:

  • Should center on a task when double click on the grid cell (to allow easy long project navigation)
  • Should open a custom popup of mine when double clicking on a task bar in the gantt (to show details about the task with my custom logic).
  • Should be able to open/collapse the “grid” part easily.

Using this code, with the React Wrapper:

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import "@bryntum/gantt/gantt.material.css";
import type { GanttBase, GanttConfig } from "@bryntum/gantt";
import { BryntumGantt } from "@bryntum/gantt-react";
import * as React from "react";

const serverData = [
  {
    children: [
      {
        children: false,
        draggable: false,
        endDate: "2021-10-30T00:00:00.000Z",
        id: "5a33440c-c1b5-4945-a999-edb80c774ab4",
        index: 0,
        manuallyScheduled: true,
        name: "test",
        parentId: "357fb463-97aa-40b9-8ef3-f8ec1dc88f88",
        percentDone: 0,
        resizable: false,
        startDate: "2021-10-01T00:00:00.000Z",
        taskNumber: "1.1",
        type: "subtask",
      },
    ],
    draggable: true,
    endDate: "2021-10-30T00:00:00.000Z",
    expanded: true,
    id: "357fb463-97aa-40b9-8ef3-f8ec1dc88f88",
    manuallyScheduled: true,
    name: "build a wall (Link to Tour Eiffel project)",
    percentDone: 100,
    resizable: true,
    startDate: "2021-10-01T00:00:00.000Z",
    taskNumber: "1",
    type: "task",
  },
  {
    children: [],
    draggable: true,
    endDate: "2021-10-26T00:00:00.000Z",
    expanded: false,
    id: "af098e9c-0ae4-4821-9d29-bf12ce30e823",
    manuallyScheduled: true,
    name: "talk to contractor",
    resizable: true,
    startDate: "2021-10-24T00:00:00.000Z",
    taskNumber: "2",
    type: "task",
  },
  {
    children: [],
    draggable: true,
    endDate: undefined,
    expanded: false,
    id: "38b491a4-012d-4201-bcdd-4539a8cf5bad",
    manuallyScheduled: true,
    name: "Task without dates",
    resizable: true,
    startDate: undefined,
    taskNumber: "3",
    type: "task",
  },
];

type Config = Partial<GanttConfig> | any;

function Gantt() {
  const ganttRef = React.useRef<{ instance: GanttBase }>();
  const [tasks, setTasks] = React.useState(serverData);
  const [taskSelected, setTaskSelected] = React.useState(null);

  const config: Config = {
    barMargin: 10,
    columns: [
      { field: "taskNumber", text: "N°", type: "number", width: 10 },
      { field: "name", type: "name", width: 50 },
    ],
    features: {
      // We don't want to use the gantt default edit form
      cellEdit: false,
      columnAutoWidth: true,
      columnLines: false,
      // We don't need the task/link dependencies
      dependencies: false,
      dependencyEdit: false,
      nonWorkingTime: false,
      // Used to have the percent of progress of a task (validated / subtasks progress)
      percentBar: {
        allowResize: false,
        showPercentage: true,
      },
      // Used to get the "red line" to show what's the progress on the planning
      progressLine: true,
      // Should show projectStart projectEnd markers
      // TODO: Find why it doesn't work as expected
      projectLines: {
        // TODO: find out why this doesn't show the "Today" marker
        showCurrentTimeLine: {
          name: "Today",
        },
        showHeaderElements: true,
      },
      taskCopyPaste: false,
      // Should be able to change date of a task with drag/drop
      // TODO: we should handle all thoses events in offline mode
      taskDrag: true,
      // It's not really "drag to create" but more "assigne" start/end date to tasks
      // who doesn't already have one
      // TODO: actually use this to open the "create task" dialog with prefill start/end date
      taskDragCreate: true,
      // We don't want to use the default taskEdit form, instead we open our own dialog
      taskEdit: false,
      taskMenu: false,
      taskResize: {
        allowResizeToZero: false,
      },
      taskTooltip: true,
      timeRanges: {
        showHeaderElements: true,
      },
      tree: true,
    },
    // Keep the scroll within project range
    infiniteScroll: true,
    maxHeight: "100vh",
    maxWidth: "100vw",
    // set max zoom level to day
    maxZoomLevel: 11,
    // set minimal zoom level to acceptable values (multiples years with quarters)
    minZoomLevel: 5,
    tbar: [
      {
        checked: true,
        label: "Show Project Line",
        listeners: {
          change: ({ checked }: { checked: boolean }) => {
            if (ganttRef.current?.instance.features.progressLine) {
              ganttRef.current.instance.features.progressLine.disabled =
                !checked;
            }
          },
        },
        type: "checkbox",
      },
      {
        inputWidth: "7em",
        label: "Project Status Date",
        listeners: {
          change: ({ value }: { value: Date }) => {
            if (ganttRef.current?.instance.features.progressLine) {
              ganttRef.current.instance.features.progressLine.statusDate =
                value;
            }
          },
        },
        step: "1d",
        type: "datefield",
        value: new Date(),
      },
    ],
    viewPreset: {
      // By default, show (day, week, month) on the same timebar
      headers: [
        {
          align: "start",
          renderer: (startDate: Date) => startDate.getMonth(),
          unit: "month",
        },
        {
          renderer: (startDate: Date) => startDate.getDate(),
          unit: "week",
        },
        {
          dateFormat: "DD",
          unit: "day",
        },
      ],
      timeResolution: {
        increment: 1,
        unit: "day",
      },
    },
  };
  return (
    <div style={{ height: "90vh", width: "90vw" }}>
      <div
        style={{
          backgroundColor: "#ecf0f1",
          display: taskSelected === null ? "none" : "block",
          height: "100vh",
          inset: 0,
          position: "absolute",
          width: "100vw",
          zIndex: 999,
        }}
      >
        Popup for {JSON.stringify(taskSelected)}
        <button
          style={{
            backgroundColor: "red",
            borderRadius: "10px",
            height: "50px",
            width: "150px",
          }}
          onClick={() => setTaskSelected(null)}
        >
          Close popup
        </button>
      </div>
      <BryntumGantt
        {...config}
        ref={ganttRef}
        tasks={tasks}
        onTaskDblClick={(e: any) => {
          const { taskRecord } = e;
          setTaskSelected(taskRecord.originalData);
        }}
        onTaskDrop={({ taskRecords }: { taskRecords: any[] }) => {
          const taskRecord = taskRecords[0];
          const taskData = taskRecord.originalData;
          const filteredTasks = tasks.filter((t) => t.id !== taskData.id);
          // Save change into the store
          setTasks([
            ...filteredTasks,
            {
              ...taskData,
              endDate: taskRecord.endDate,
              startDate: taskRecord.startDate,
            },
          ]);
        }}
        onCellDblClick={(e: any) => {
          // extract the record from the event
          const { record } = e;
          ganttRef.current?.instance?.scrollTaskIntoView(record, {
            animate: true,
            block: "center",
          });
        }}
      />
    </div>
  );
}

export { Gantt };

I've been able to achieve everything I want pretty nicely:

Screen Recording 2021-10-26 at 17.05.37.mov
(13.2 MiB) Downloaded 52 times

However, when I'm dealing with touch devices (here in the google chrome mobile device emulator), it seems that my logic isn't called anymore:

Screen Recording 2021-10-26 at 17.08.27.mov
(4.7 MiB) Downloaded 47 times

It's probably related to touch instead of click actions. Issue is but I haven't found anything related to this in the documentation.

How could I intercept the "double-touch | longpress" events on the grid and on the Gantt tasks bar ? Did I missed some properties ?
Also, I didn't found a way to "keep the grid collapse" buttons always visible to the user with the buttons for "collapse/open/fullsize" accessible on mobile ? (without the need to drag it, which can be tedious for users, and can be produce blinking).

PS: Thank's for the amazing work everyone at Bryntum do to make this library such an useful tool. From what I've seen it's the best on the market so far, both in terms of features and developer experience.


Post by Maxim Gorkovsky »

Hello.
On pure touch devices there is no doubletouch event, only these are supported: https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent Some platforms emulate doubletap but measuring time between two touchstart events. This is what you can do too: add listener to taskclick event, launch some sort of timer, if 2nd taskclick event occurred in about 100ms - open task editor. https://bryntum.com/docs/gantt/api/Gantt/view/GanttBase#function-editTask

Speaking of splitter, I reproduced the problem and opened a ticket here: https://github.com/bryntum/support/issues/3627
You can try to customize CSS to make splitter wider and show buttons. On touch devices we add .b-touch class to main widget element.


Post by Maxim Gorkovsky »

Speaking of scrolling task into view, in the coming release we will add a config called scrollTaskIntoViewOnCellClick which will do exactly what you expect and it also will work (I've just checked) on chrome-emulated touch device.


Post by avallete »

Nice thank's for your answer ! It'll be looking forward to the next release then :)

Also, I'll be trying your solution of manually implementing doubletap form "click" event. Thank you !


Post Reply