Our state of the art Gantt chart


Post by avallete »

Hi there !

Using the React wrapper alongside changing tasks data I had trouble getting my "new data" re-rendered.

At first I was trying to update my data by calling the store "load" function like so:

  const ganttRef = React.useRef<{ instance: GanttBase }>();

  React.useEffect(() => {
    if (ganttRef.current) {
      ganttRef.current.instance.taskStore.data = props.tasks;
    }
  }, [ganttRef.current, JSON.stringify(props.tasks)]);

Doing so, I have the data "updated" but not everywhere. The data rows into the "Grid" is updated properly (when name change), but every other changes (percentDone, startDate, endDate) on the data isn't propagated to the gantt.

So I changed strategy and use this method instead:

  const ganttConfig: Partial<GanttConfig> = {
    ...config,
    project: {
      ...config.project,
      eventsData: props.tasks,
    },
  };

This seems the resolve the issue (all the data is properly updated on both the grid and the gantt), but I'm wondering if this doesn't come with some side effect, and if a better way to achieve what I want exist ?

Last edited by avallete on Wed Oct 27, 2021 9:56 am, edited 2 times in total.

Post by saki »

The idea of wrappers is to make the resulting BryntumGantt component as React-ish as possible. Therefore, using the state and mapping tasks to the prop (without other code) should do the trick:

const [tasks, setTasks] = useState();

// ... anything that loads of changes the tasks

return (
    <BryntumGantt {...ganttConfig} tasks={tasks} />
)

Note: you may need to set syncDataOnLoad to false on the task store. See viewtopic.php?p=94993#p94993 for details.


Post by avallete »

I've already tried to use only the "tasks" property of the wrapper component (it was my first approach), but the values won't update properly (the behavior is similar to the issue I have with "taskStore.data = props.tasks", the grid data update but not the gantt).

And when I try to add the "syncDataOnLoad" option with the "tasks" props, I have an error raising from the constructor: "Uncaught Error: Providing both project and inline data is not supported".


Post by saki »

Post please your code that we could run, investigate and debug. Wrapper should handle the tasks property correctly and pass it down to the project.taskStore. We need to find the reason why it is not happening.


Post by avallete »

Hi there,

Thank you for the response. I've been able to make an isolated MRE repoducing the issue I had in my app:

import "@bryntum/gantt/gantt.material.css";
import type { GanttBase } from "@bryntum/gantt";
import { BryntumGantt } from "@bryntum/gantt-react";
import * as React from "react";

const serverData = [
  {
    children: [
      {
        children: false,
        draggable: false,
        endDate: "2045-06-22T00:00:00.000Z",
        id: "5a33440c-c1b5-4945-a999-edb80c774ab4",
        index: 0,
        name: "test",
        parentId: "357fb463-97aa-40b9-8ef3-f8ec1dc88f88",
        percentDone: 0,
        resizable: false,
        startDate: "2021-06-22T00:00:00.000Z",
        taskNumber: "1.1",
        type: "subtask",
      },
    ],
    endDate: "2045-06-22T00: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-06-22T00:00:00.000Z",
    taskNumber: "1",
    type: "task",
  },
  {
    children: [],
    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: [],
    endDate: "2021-10-29T00:00:00.000Z",
    expanded: false,
    id: "38b491a4-012d-4201-bcdd-4539a8cf5bad",
    manuallyScheduled: true,
    name: "Some other task",
    resizable: true,
    startDate: "2021-10-21T00:00:00.000Z",
    taskNumber: "3",
    type: "task",
  },
  {
    children: [
      {
        children: false,
        draggable: false,
        endDate: "2021-10-27T00:00:00.000Z",
        id: "8e1a1dd6-fbef-4a23-a07e-ef1d2f09e421",
        index: 0,
        name: "One",
        parentId: "8fbe4826-be86-459d-8a03-4c76471dbc90",
        percentDone: 0,
        resizable: false,
        startDate: "2021-10-22T00:00:00.000Z",
        taskNumber: "4.1",
        type: "subtask",
      },
      {
        children: false,
        draggable: false,
        endDate: "2021-10-27T00:00:00.000Z",
        id: "e9dccba4-643b-4244-aa99-7d40a0249663",
        index: 1,
        name: "Twi",
        parentId: "8fbe4826-be86-459d-8a03-4c76471dbc90",
        percentDone: 0,
        resizable: false,
        startDate: "2021-10-22T00:00:00.000Z",
        taskNumber: "4.2",
        type: "subtask",
      },
      {
        children: false,
        draggable: false,
        endDate: "2021-10-27T00:00:00.000Z",
        id: "2fe0e374-7dc2-4e78-8929-876ac60606b7",
        index: 2,
        name: "Three",
        parentId: "8fbe4826-be86-459d-8a03-4c76471dbc90",
        percentDone: 0,
        resizable: false,
        startDate: "2021-10-22T00:00:00.000Z",
        taskNumber: "4.3",
        type: "subtask",
      },
    ],
    endDate: "2021-10-27T00:00:00.000Z",
    expanded: true,
    id: "8fbe4826-be86-459d-8a03-4c76471dbc90",
    manuallyScheduled: true,
    name: "Task with childs",
    percentDone: 0,
    resizable: true,
    startDate: "2021-10-22T00:00:00.000Z",
    taskNumber: "4",
    type: "task",
  },
];

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

  React.useEffect(() => {
    setTimeout(() => {
      console.log("Called after 10seconds !");
      const modifiedData = JSON.parse(JSON.stringify(serverData));
      modifiedData[0].children[0].percentDone = 100;
      setTasks(modifiedData);
    }, 10000);
  }, []);

  React.useEffect(() => {
    setTimeout(() => {
      console.log("Called after 50seconds !");
      const modifiedData = JSON.parse(JSON.stringify(serverData));
      modifiedData[0].name = "a new name for this task";
      setTasks(modifiedData);
    }, 30000);
  }, []);

  const ganttConfig = {
    allowCreate: true,
    barMargin: 10,
    columns: [
      { field: "taskNumber", text: "N°", type: "number", width: 10 },
      { field: "name", type: "name", width: 50 },
    ],
    features: {
      cellEdit: false,
      columnAutoWidth: true,
      columnLines: false,
      dependencies: false,
      dependencyEdit: false,
      nonWorkingTime: false,
      percentBar: {
        allowResize: false,
        showPercentage: true,
      },
      projectLines: true,
      taskContextMenu: false,
      taskCopyPaste: false,
      taskDrag: true,
      taskDragCreate: true,
      taskEdit: false,
      taskMenu: false,
      taskResize: {
        allowResizeToZero: false,
      },
      taskTooltip: true,
      timeRanges: {
        showHeaderElements: true,
      },
      tree: true,
    },
    infiniteScroll: true,
    viewPreset: {
      headers: [
        {
          align: "start",
          renderer: (startDate: Date) => new Date(startDate).getMonth(),
          unit: "month",
        },
        {
          dateFormat: "DD",
          unit: "day",
        },
      ],
      timeResolution: {
        increment: 1,
        unit: "day",
      },
    },
  };
  console.log("Tasks now are: ", tasks);
  return (
    <div style={{ height: "100vh", width: "90vw" }}>
      <BryntumGantt {...ganttConfig} tasks={tasks} ref={ganttRef} />
    </div>
  );
}

export { Gantt };

Simply importing this component as "<Gantt />" into any react project should show the problem. After the 1st timeout of 10s, you will not see the state of the "percentDone" bar change for the children of the 1st task. However, after the change of the name (30s timeout), you'll see the new updated name properly displayed into the Grid.


Post by saki »

There is a known bug related to it https://github.com/bryntum/support/issues/2944 To workaround, set

    taskStore : {
        syncDataOnLoad : false
    },

in your ganttConfig and it will work as expected.


Post by avallete »

Hi there !

Thank you so much for your solution !

Indeed, I didn't used the "taskStore" directly into the props before because according to the "GanttConfig" type, this should contain a "model". But I was putting it inside:

project: {
    taskStore : {
        syncDataOnLoad : false
    },
 }

And this, doesn't work.

However, bypassing this type and using directly the syntax, you provided alongside the "tasks" as props allow me to have fully reactive data. Only issue it cause me is that I was using this syntax to get a "Today" line on my Gantt:

    project: {
      timeRangesData: [
        {
          duration: 0,
          durationUnit: "d",
          id: 1,
          name: t("gantt.Today", "Today"),
          startDate: new Date(),
        },
      ],
    },
 

And this syntax, which use "project" definition, isn't allowed with the "props tasks" syntax ("providing inline data and project error"). Is there any other way to add "markers" inside the react wrapper ? (I don't know if I should put this into another topic since the main subject (reactive data) has been resolved with your solution)

Since the component doesn't have type definitions for his props, doesn't really seems to exactly match the types of "GanttConfig" either it's not easy to know which options from the API Documentation I can or cannot use.

PS:

I also tried this syntax mentioned in the documentation here: https://www.bryntum.com/docs/gantt/api/Gantt/feature/ProjectLines#config-showCurrentTimeLine without any luck (I have the "Project Start" and "Project End" markers, but not the "Today" marker).

const ganttConfig = {
   ...
   features: {
     projectLines: {
        showCurrentTimeLine: {
          name: "Today",
        },
        showHeaderElements: true,
     },
   }
}

Post by saki »

Post please your complete Gantt.tsx file, I'll take a look.


Post by avallete »

Hi there thank you for your time.

Here I made a minimal reproduction example of my gantt configuration (I simply extracted the data sync logic with my backend to have it statically set):

/* 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: "2021-10-29T00:00:00.000Z",
    expanded: false,
    id: "38b491a4-012d-4201-bcdd-4539a8cf5bad",
    manuallyScheduled: true,
    name: "Some other task",
    resizable: true,
    startDate: "2021-10-21T00:00:00.000Z",
    taskNumber: "3",
    type: "task",
  },
];

type Config = Partial<GanttConfig> | any;

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

  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 and Today markers
      projectLines: {
        // TODO: find out why this doesn't show the "Today" marker
        showCurrentTimeLine: {
          name: "Today",
        },
        showHeaderElements: true,
      },
      taskContextMenu: false,
      taskCopyPaste: false,
      // Should be able to change date of a task with drag/drop
      taskDrag: true,
      // It's not really "drag to create" but more "schedule" start/end date to tasks
      // who doesn't already have one
      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: false,
    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,
    // We need this to have reactive data
    // see: https://github.com/bryntum/support/issues/2944
    taskStore: {
      syncDataOnLoad: false,
    },
    tbar: [
      {
        checked: true,
        label: "Show Project Line",
        listeners: {
          change: ({ checked }: { checked: boolean }) => {
            console.log(ganttRef.current);
            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" }}>
      {/* Make it fit inside the "screen" grid */}
      <BryntumGantt
        {...config}
        ref={ganttRef}
        tasks={tasks}
        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,
            },
          ]);
        }}
      />
    </div>
  );
}

export { Gantt };

Doing so, I wonder if there is any recommended way to provide code examples with gantt-lib + react wrapper (something like: https://stackblitz.com) as it would probably be easier to analyse/share. Since the packages are located on private repositories (and the react wrapper need a package aliasing) I haven't been able to put my code reproduction on such platform.


Post by saki »

Thank you for the code you provided, it helped a lot. To your questions plus some advices:

  1. showCurrentTimeline should be defined on timeRanges features:

                timeRanges: {
                    showHeaderElements: true,
                    showCurrentTimeLine: {
                        name: 'Today'
                    }
                },
  2. The types are not present because they are turned off by line:

    type Config = Partial<GanttConfig> | any;

    To have the correct typing we need to remove "| any" from the type. The consequence is that TypeScript complains about

            taskStore: {
                syncDataOnLoad: false
            },

    It can be easily solved by removing this block from config and setting it directly on the tag:

        return (
            <BryntumGantt
                {...config}
                ref={ganttRef}
                tasks={tasks}
                taskStore={{syncDataOnLoad:false}}

    which has the same effect w/o TS complaining.

    Then we have all types present:

    Screen Shot 2021-10-26 at 18.13.34.png
    Screen Shot 2021-10-26 at 18.13.34.png (183.35 KiB) Viewed 583 times

    \3. Thank you for info on StackBlitz, we will discuss it internally.


Post Reply