Our state of the art Gantt chart


Post by greg.user1 »

I have a two tasks: T1 and T2.
T2 is a children of T1 and I'm using flat data to load it to the gantt:

[
  { id : 1, name : 'T1' },
  { id : 2, name : 'T2', parentId : 1 }
]

In my task store I have 'tree' and 'transformFlatData' set to true.
When the page load it all looks fine, by adding for example new task is causing an error (there is more operations causing this error):

Id collision on 2

I was investigating the issue and found out that when the page load the task store stores 3 records rather than two:
T1 (with T2 inside the children array) and T2 as a task on the same level as T1:

T1 
    -T2
T2

I guess this should be like that instead:

T1 
    -T2

Any idea what may cause it?

Thanks in advance


Post by mats »

Which version are you using? Could you please share your exact code too?


Post by greg.user1 »

Hi Mats,

Unfortunately sharing a code won't be possible as I'm working on a big project where Bytntum Gantt is only one of many components on the page.

I'm using version 5.0.5.

I'm just passing a data (similar to the example above) to the project object like that:

const project = useRef<ProjectModel>(
    new ProjectModel({
      taskStore: {
        autoTree: true,
        transformFlatData: true,
        data,
        ....
      },
      onDataReady: onDataReady,
      ....
    })
  );

Then in onDataReady I have a breakpoint and inspecting allRecords of project.current.taskStore shows the issue where T2 is children of T1 but also it's on the same level in the array as T1 as I explained above.


Post by mats »

That's the correct behavior, allRecords returns a flat record set of every record in the tree. What do you use allRecords where it becomes a problem?


Post by greg.user1 »

I don't really use allRecords, I just thought the issue is with this store as this looked to me as a mix of flat and tree data.

After you said that's the correct behaviour I was further investigated my issue and I manage to replicate the bug.
The 'id collision' error happens only when I pass my data as a prop and then call React setState.
This is a minimal code example which you could try yourself, just run it and hit 'Add new Task' button, please:

// Table2.tsx file
const Table2 = ({ data }: any) => {
  const [canUndo, setCanUndo] = useState(false);

  const onDataReady = () => {
    if (project.current.stm.disabled === true) {
      project.current.stm.enable();
    }
  };

  const setUndoRedoStates = () => {
    setCanUndo(project.current.stm.canUndo);
  };

  const project = useRef<ProjectModel>(
    new ProjectModel({
      stm: {
        autoRecord: true,
        listeners: {
          recordingStop: () => setUndoRedoStates(),
          restoringStop: () => setUndoRedoStates(),
        },
      },
      taskStore: {
        autoTree: true,
        transformFlatData: true,
        data,
      },
      onDataReady: onDataReady,
    })
  );

  const ganttConfig = {
    viewPreset: "weekAndDayLetter",
    columns: [{ type: "name", field: "name", width: 250 }],
  };

  const addTask = () => {
    project.current.taskStore.add({
      id: 3,
      name: "New task",
      duration: 1,
      startDate: new Date(),
    });
  };

  return (
    <>
      <button onClick={addTask}>Add new Task</button>
      <BryntumGantt {...ganttConfig} project={project.current} />
    </>
  );
};

export default Table2;

I use this component in my root file, something like that:

// App.tsx file
function App() {
  const data = [
    { id: 1, name: "Parent" },
    { id: 2, name: "Child", parentId: 1 },
  ];
  return (
    <>
      <Table2 data={data} />
    </>
  );
}

When I move the data from props directly to the Table2 all is working as expected.
Also if I remove the line which set the state:

setCanUndo(project.current.stm.canUndo);

this works.

I need to pass the data as a prop and I'm using setState in few places so this is a requirement for me.
Would you be able to advice on how to fix this one please?

BTW.
If I'm using useRef for ProjectModel

const project = useRef<ProjectModel>(.....);

as in this example above the data is not render in React 18 in strict mode. I had to remove strict mode to make it work.
Are you aware of this? I hope this will be resolved in the future.

Many thanks


Post by saki »

First of all, try to add new task without id. ids must be unique but addTask method always uses the same id.


Post by greg.user1 »

Hi Saki,

Thanks for swift response.

I deleted the id from addTask function and still have the same error: 'Id collision on 2'.

I hardcoded the id here only for demo purposes, in my app I auto increment it.
I have to use custom ids, but I'm making sure this is unique.

If you try to run this example you can see that this error shows up after you click (just once) on add task (even if you delete id as per your suggestion).
Also the error complains about collision on 2 but I'm trying to add task with id 3 (in this example).


Post by greg.user1 »

Hi Saki,

Any update on this one?

Thanks


Post by marcio »

Hey Greg,

Checking the example that you provided, it seems that the main thing is that you're using useRef, and the re-render process triggers an update in the project config, and the config is not updatable in the runtime. I'm sharing an example using useMemo, and with that, is working just fine.

import { useState, useMemo, useCallback } from 'react';
import React from 'react';
import { BryntumGantt } from '@bryntum/gantt-react';
import { ProjectModel } from '@bryntum/gantt';

const Table2 = ({ data }) => {
  const [canUndo, setCanUndo] = useState(false);

  const setUndoRedoStates = useCallback((projectModel) => {
    setCanUndo(projectModel.stm.canUndo)
  }, []);

  const onDataChange = useCallback((projectModel) => {
    if (projectModel.stm.disabled === true) {
      projectModel.stm.enable();
    }
  }, [])

  const project = useMemo(() =>
    new ProjectModel({
      stm: {
        autoRecord: true,
        listeners: {
          recordingStop: () => setUndoRedoStates(project),
          restoringStop: () => setUndoRedoStates(project),
        },
      },
      taskStore: {
        autoTree: true,
        transformFlatData: true,
        data
      },
      onDataReady: () => onDataChange(project),
    }), [data, onDataChange, setUndoRedoStates]
  );

  const ganttConfig = {
    viewPreset: "weekAndDayLetter",
    columns: [{ type: "name", field: "name", width: 250 }],
  };

  const addTask = () => {
    project.taskStore.add({
      id: 3,
      name: "New task",
      duration: 1,
      startDate: new Date(),
    });
  };

  return (
    <>
      <button onClick={addTask}>Add new Task</button>
      <BryntumGantt {...ganttConfig} project={project} />
    </>
  );
};

export default Table2;

Best regards,
Márcio


Post by greg.user1 »

Hi Marcio,

I'm using useRef because I'm sharing the project with Timeline and TaskBoard, if I use useMemo this won't work.
The useRef comes with one of your example (Timeline).
Could you advise some solution which takes the data sharing into consideration, please?


Post Reply