Our state of the art Gantt chart


Post by tanu agarwal »

Hello Team

We are facing problem while fetching the newly added task to the Grid (we have action column to open a Add task modal). Please check the attached Add Task column code and related columns to see what's the problem is.

  1. My implementation for adding a new task involves opening a Modal (using props.openModal), collecting the details, and then submitting them via an API call. Everything is working till here.

Upon successful submission, I attempt to reflect the new data in the Gantt grid instantly by calling the ganttInstance.taskStore.add method with the task object (data from the Add task modal).

The current behavior is inconsistent: Core fields like the task's Name and importance update as expected, achieving the instant reflection. However, secondary fields—specifically status, functionsCount, and documents Count—are missing from the grid until the application is refreshed. We need these fields to display immediately. (Please check screenshot Bryntum-test, Bryntum Test-2, Bryntum Test 3 - only put 1 as not able to upload more thn 3 files).

  1. After adding a Task the icon is inconsistent, We have after render method which will calculate the icon according to the Level, but as soon as i add any Task it started showing L3 icon all the time till the time i refresh the page.

  2. Column filter : when we are selecting the filters for date column and than delete filter from trash icon. the filter is still displayed on the screen in half broken state. Please see the screenshot attached.

  3. Grid WBS Refresh : WBS for the grids are auto changing even if it is saved correctly in DB. On the bryntum grid WBS changes to something else and reorganise itself.

5.When there is no start date-end date in the grid. I tried to select a start date it will ask me to select end date first. And sometime calendar control also moved to top left corner of the screen while selecting.

Add task column code

{
                            type: 'action', width: 50, region: 'left',
                            actions: [{
                                cls: 'add-new-task',
                                visible: ({ record }) => (record['level'] === TaskOutlineLevel.Level1) ? false : ((record['level'] < 5) &&
                                    (props.projectData.ProjectAccess === UserProjectAccess.PrimaryLead) ||
                                    (props.projectData.ProjectAccess === UserProjectAccess.SecondaryLead)) ||
                                    (record['level'] > 2 && (record['taskAccess'] === UserTaskAccess.Owner ||
                                        record['taskAccess'] === UserTaskAccess.CoOwner))
                                ,
                                tooltip: 'Add new task',
                                onClick: ({ record }) => {
                                    props.openModal(
                                        <AddTask
                                            projectId={props.projectGuid}
                                            webpartContext={props.webpartContext}
                                            popUpDetails={record}
                                            projectAccess={props.projectData.ProjectAccess}
                                            onUpdateAddTask={async (param: any) => {
                                                const ganttInstance = ganttRef.current?.instance;
                                                const isParantLevel = record['level'] === param.Level;
                                                const parentId = isParantLevel ? record.parentId : record.id;
                                                const assignments: string[] = [param.Owner, ...param.CoOwners];

                                            const taskRec = ganttInstance.taskStore.add({
                                                id: param.id,
                                                name: param.TaskName,
                                                parentId,
                                                startDate: param.StartDate,
                                                endDate: param.EndDate,
                                                lastUpdatedBy: props.webpartContext?.pageContext?.user?.email?.toLowerCase() || "",
                                                importance: param.importance,
                                                statusName: TaskStatus.NotStarted,
                                                functionsCount: 0,
                                                documentsCount: 0
                                            });
                                            await ganttInstance.project.commitAsync();
                                            ganttInstance.renderRows();
                                        }}
                                    />
                                )
                            }
                        }],
                        filterable: false,
                        sortable: false,
                        editor: false,
                    },

Documentation Column Code

{
                            text: "Documentation",
                            field: "documentsCount",
                            editor: false, readonly: true,
                            align: "center", width: 150,
                            renderer: ({ record }) => {
                                return record['level'] > TaskOutlineLevel.Level1 ?
                                    <div className='documentWrapper'>
                                        <DefaultButton
                                            className='documents-cls'
                                            text={record.documentsCount}
                                            title={`${record.documentsCount} Documents`}
                                            disabled={IsTemplateTask(record)}
                                            onClick={() => onDocumentsActionClicked("view", record)}
                                        />
                                        {(record["level"] >= 2 && (props.projectData.ProjectAccess === UserProjectAccess.PrimaryLead || props.projectData.ProjectAccess === UserProjectAccess.SecondaryLead ||
                                            record["taskAccess"] === UserTaskAccess.Owner || record["taskAccess"] === UserTaskAccess.CoOwner)) && <ActionButton
                                                iconProps={{ iconName: "CloudUpload" }}
                                                title='Upload Documents'
                                                disabled={IsTemplateTask(record)}
                                                onClick={() => onDocumentsActionClicked("upload", record)}
                                            >
                                            </ActionButton>}
                                    </div>
                                    :
                                    ''
                            }
                        },

Function column code

{
                            text: "Functions", field: "functionsCount",
                            editor: false, readonly: true,
                            align: "center", width: 100,
                            renderer: ({ record }) => {
                                return record['level'] > TaskOutlineLevel.Level1 ? <DefaultButton
                                    className='documents-cls'
                                    text={record.functionsCount}
                                    title={`${record.functionsCount} functions`}
                                    disabled={IsTemplateTask(record)}
                                    onClick={() =>
                                        props.openModal(<TaskFunctionsModal
                                            projectGuid={props.projectGuid}
                                            webpartContext={props.webpartContext}
                                            record={record}
                                            projectData={props.projectData}
                                            onUpdateTaskFunctionsCount={(param: any) => {
                                                record.set({ functionsCount: param });
                                            }} />)
                                    }
                                /> : ""
                            }
                        },

After render method to calculate Task icon logic

   {
                            id: 'activity', type: 'name', text: 'Task', sortable: true, filterable: true, width: 350, region: 'left',
                            tooltipRenderer: ({ record }) => record["name"],
                            afterRenderCell: (props) => {
                                const { cellElement, record, grid, row } = props;
                                const exp = cellElement.querySelector('.b-tree-expander, .b-tree-toggle, .b-tree-icon');

                            if (!exp) return;

                            exp.classList.remove('b-icon-tree-collapse');
                            exp.classList.remove('b-icon-tree-expand');
                            exp.classList.remove('b-icon-tree-leaf');
                            exp.classList.remove('first-level-expand-icon');
                            exp.classList.remove('first-level-collapsed-icon');
                            exp.classList.remove('second-level-expand-icon');
                            exp.classList.remove('second-level-collapsed-icon');

                            exp.classList.remove('level-expanded');
                            exp.classList.remove('first-level-icon');
                            exp.classList.remove('second-level-icon');
                            exp.classList.remove('third-level-icon');
                            exp.classList.remove('leaf-level-icon');

                            const levelClassMap: Record<number, string> = {
                                [TaskOutlineLevel.Level1]: "first-level-icon",
                                [TaskOutlineLevel.Level2]: "second-level-icon",
                                [TaskOutlineLevel.Level3]: "third-level-icon",
                                [TaskOutlineLevel.Level4]: "leaf-level-icon"
                            };

                            const levelClass = levelClassMap[record.level] || "leaf-level-icon";
                            exp.classList.add(levelClass);

                            if (record.level >= TaskOutlineLevel.Level3 && record.isLeaf) {
                                exp.classList.remove('third-level-icon');
                                exp.classList.remove('leaf-level-icon');
                                exp.classList.add('leaf-level-icon');
                            }

                            const expanded = record.isExpanded(grid.taskStore);
                            if (expanded) {
                                exp.classList.add("level-expanded");
                            }
                        },
                        expandIconCls: 'iconCls',
                        collapseIconCls: 'iconCls',
                        leafIconCls: 'iconCls leaf-level-icon',
                        finalizeCellEdit: ({ value }) => {
                            return value.length === 0 ? 'Please enter a name.' : true;
                        }
                    },

[url]
https://mytakeda-my.sharepoint.com/:v:/r/personal/ashish_surve_takeda_com/Documents/Microsoft%20Teams%20Chat%20Files/Bryntum%20Test.mp4?csf=1&web=1&e=U9aWy9
[url]

Attachments
Broken Filter
Broken Filter
Screenshot 2025-10-31 135805.png (32.7 KiB) Viewed 1573 times
Icon issue
Icon issue
Bryntum-test-3-Icon-issue.png (123.36 KiB) Viewed 1573 times
Add task auto refresh not working
Add task auto refresh not working
Bryntum-test.png (119.44 KiB) Viewed 1573 times

Post by alex.l »

Hi,

Please make sure you extended your TaskModel with all custom fields you need https://bryntum.com/products/gantt/docs/api/Gantt/model/TaskModel#subclassing-the-taskmodel-class

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy


Post by ashishtakeda »

Hello Guys,

Please see the attached video below, we have a column which will show the owner details, to add/replace the current owner we have a modal, from there user can change the current owner, what we are trying to achieve is upon successfully submission i want the selected owner should immediately reflect (without refreshing the page) in the cell, can you please help us in that?

Once we get the answer for the owner column we will implement the same logic for co-owner as well.

##code for owner column:

{
                            id: 'owner',
                            type: 'column',
                            text: 'Owner',
                            width: 120,
                            editor: false,
                            renderer: ({ record, grid }) => {
                                let owner = undefined;

                            if (record.assignments.length > 0) {
                                const resourceMap = new Map(record.resources.map(r => [r.id, r]));
                                for (const assignment of record.assignments) {
                                    const resource = resourceMap.get(assignment.resourceId);
                                    if (!resource) continue;

                                    if (assignment.isOwner) {
                                        owner = resource;
                                    }
                                }
                            }

                            return record['level'] > TaskOutlineLevel.Level1 ?
                                <div className="d-flex">
                                    {owner ?
                                        <UserPersona
                                            resources={[{
                                                id: owner.id,
                                                ResourceName: owner.name,
                                                ResourceEmailAddress: owner.emailAddress
                                            }]}
                                            maxDisplayablePersonas={1}
                                            hidePersonaDetails={true}
                                        /> : ''}

                                    {
                                        <ActionButton
                                            styles={panelButtonsStyles}
                                            iconProps={{ iconName: 'EditContact' }}
                                            onClick={() => handleOwnerClick(owner, record)}
                                            title="Update Owner"
                                        >
                                        </ActionButton>
                                    }
                                </div> : ""
                        }
                    },

##handle click to open the modal ( to get the values from the modal i am using callback "onUpdateTaskOwnerCoOwner")

const handleOwnerClick = (selectedOption: ISelectedOption, record: any) => {
        const ganttInstance = ganttRef.current?.instance;
        const options = mapApiDataToDropdownOptions(ganttInstance.project.resources.map(model => model.data));
        props.openModal(<OwnerCoOwnerModal
            onUpdateTaskOwnerCoOwner={async (param: any) => {
                updateTaskAssignments(record, [param], true);
            }}
            taskId={record.id}
            projectGuid={props.projectGuid}
            webpartContext={props.webpartContext}
            options={options}
            selectedOption={selectedOption} isOwner={true} />)
    }

Attachments
Bryntum Add owner co-owner refresh-1.mp4
add owner
(26.8 MiB) Downloaded 14 times

Post by alex.l »

Could you please share version of Gantt you used? Try with the latest one.

Pretty hard to guess without runnable test case and debugging. Any chance to get it?
And please try to remove React cmp from the column renderer, try to place simple text and see if the problem in component re-render or in column renderer itself?

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy


Post by ashishtakeda »

Hello guys,

Please see the runnable test. Below, I have provided the code—just copy and paste it into your demo project. Please read the comment saying "comments from Takeda." Most importantly, please check the video I attached in the previous post for a better understanding of the requirement.

Let me explain the problem again:

We have two columns, Owner and Co-owner (for now, let's consider the Owner column). We are using a Gantt chart (version 6.2.2), and the column is a custom column (I have provided the actual column code again for your reference). We are rendering the owner's name, and beside it, we have a button. Once you click on the button, a modal opens, and from the modal, you can change the current owner from the dropdown. After saving, the modal closes. Everything is working correctly up to this point.

What we need: Once a new owner is selected and the modal is closed, we want the same owner to reflect in the cell without refreshing the page. This is where we need your help (please see the prevoius post video ). I am getting the selected value from the modal (using onUpdateTaskOwnerCoOwner); we need your help to set this field again so that the new owner will display there.

For the demo purpose, I have attached a handleClick function, and I want to replace the name "Alice Smith" with the name of the "task record" just to simulate the behavior. Please see the attached video of the demo.

Our Expectation: Show the selected owner's name in the cell without refreshing the page—that's it.

##copy and paste this into the Gantt demo (i am using taskeditor for the demo purpose)

/**
 * Application configuration
 */
import { TaskModel, StringHelper } from "@bryntum/gantt";
import "./components/FilesTab";

import React from "react";


class MyModel extends TaskModel {
    constructor(props) {
        super(props);
        this.state = {
            isDialogOpen: false,
        }
    }
  static get fields() {
    return [{ name: "deadline", type: "date" }, { name: "color" }];
  }
}

const handleOwner = (taskRecord) => {
    console.log("Change owner for task:", taskRecord.name);
    // comments from Takeda
    //NOTE: please check prevoius post video i have attached for better understanding of my requirement
    //tell me how do i set taskRecord.name in the cell again so that it will replace current Alice smith wihth taskRecord.name in the same cell i have clicked on

//in my example i am opening a dialog to select new owner from list (please see the video in the prevoius post i have attached for better understanding) and then make an api call, get the selected owner from the modal using callback, i will get the object in my callback something like  {id:"some-id", name:"new owner name", emailAddress:"some email"} then i will set the new owner in the cell
//so please tell me how i can set the new owner in the cell again so that i can see that instantly updated without refreshing the page
}

const ganttConfig = {
  project: {
    autoSetConstraints: true,
    autoLoad: true,
    taskModelClass: MyModel,
    transport: {
      load: {
        url: "data/launch-saas.json",
      },
    },
    // This config enables response validation and dumping of found errors to the browser console.
    // It's meant to be used as a development stage helper only so please set it to false for production systems.
    validateResponse: true,
  },

  // Shows a color field in the task editor and a color picker in the task menu.
  // Both lets the user select the Task bar's background color
  showTaskColorPickers: true,

  columns: [
    { type: "name", field: "name", text: "Name", width: 250 },
    {
      id: "owner",
      type: "column",
      text: "Owner",
      width: 150,
      editor: false,
      htmlEncode: false,
      renderer: ({ record, grid }) => {
        const staticResources = [
          {
            id: "res-101",
            name: "Alice Smith",
            emailAddress: "alice@example.com",
          },
          {
            id: "res-202",
            name: "Bob Johnson",
            emailAddress: "bob@example.com",
          },
          {
            id: "res-303",
            name: "Charlie Brown",
            emailAddress: "charlie@example.com",
          },
        ];

    const staticAssignments = [
      { resourceId: "res-101", isOwner: true },
      { resourceId: "res-202", isOwner: false },
    ];
    // comments from Takeda
    //please note that in real world i am fetching resources and assignments from record like below
    // const staticResources = record.resources;
    // const staticAssignments = record.assignments;

    const recordResources = staticResources;
    const recordAssignments = staticAssignments;

    let owner = undefined;

    if (recordAssignments.length > 0) {
      const resourceMap = new Map(recordResources.map((r) => [r.id, r]));
      for (const assignment of recordAssignments) {
        const resource = resourceMap.get(assignment.resourceId);
        if (!resource) continue;

        if (assignment.isOwner) {
          owner = resource;
        }
      }
    }

    return owner ? (
      <div>
        <span>{owner.name}</span>
        <button onClick={() => handleOwner(record)}>{"change name"}</button>
      </div>
    ) : (
      ""
    );
  },
},
  ],

  taskEditFeature: {
    items: {
      generalTab: {
        title: "Common",
        items: {
          customDivider: {
            html: "",
            dataset: {
              text: "Custom fields",
            },
            cls: "b-divider",
            flex: "1 0 100%",
          },
          deadlineField: {
            type: "datefield",
            name: "deadline",
            label: "Deadline",
            flex: "1 0 50%",
            cls: "b-inline",
          },
          priority: {
            type: "radiogroup",
            name: "priority",
            label: "Priority",
            labelWidth: "6.5em",
            flex: "1 0 100%",
            options: {
              high: "High",
              med: "Medium",
              low: "Low",
            },
          },
        },
      },
    
notesTab: false, filesTab: { type: "filestab", weight: 110, }, predecessorsTab: { items: { grid: { columns: { data: { // Our definition of the name column // is merged into the existing one. // We just add some configurations. name: { // Grid cell values are rendered with extra info. renderer({ record: dependency }) { const predecessorTask = dependency.fromTask; if (predecessorTask) { return StringHelper.xss`${predecessorTask.name} (${predecessorTask.id})`; } return ""; }, // The cell editor, and its dropdown list // also have this extra info. editor: { displayValueRenderer(taskRecord) { return taskRecord ? StringHelper.xss`${taskRecord.name} (${taskRecord.id})` : ""; }, listItemTpl(taskRecord) { return StringHelper.xss`${taskRecord.name} (${taskRecord.id})`; }, }, }, }, }, }, }, }, }, }, }; export { ganttConfig };

##Actual Owner column code

{
                            id: 'owner',
                            type: 'column',
                            text: 'Owner',
                            width: 120,
                            editor: false,
                            renderer: ({ record, grid }) => {
                                let owner = undefined;

                        if (record.assignments.length > 0) {
                            const resourceMap = new Map(record.resources.map(r => [r.id, r]));
                            for (const assignment of record.assignments) {
                                const resource = resourceMap.get(assignment.resourceId);
                                if (!resource) continue;

                                if (assignment.isOwner) {
                                    owner = resource;
                                }
                            }
                        }

                        return 
                            <div className="d-flex">
                                {owner ?
                                    <UserPersona
                                        resources={[{
                                            id: owner.id,
                                            ResourceName: owner.name,
                                            ResourceEmailAddress: owner.emailAddress
                                        }]}
                                        maxDisplayablePersonas={1}
                                        hidePersonaDetails={true}
                                    /> : ''}

                                {
                                    <ActionButton
                                        styles={panelButtonsStyles}
                                        iconProps={{ iconName: 'EditContact' }}
                                        onClick={() => handleOwnerClick(owner, record)}
                                        title="Update Owner"
                                    >
                                    </ActionButton>
                                }
                            </div> 
                    }
                },

##actual handle click code to open a modal and get the selected value from modal

const handleOwnerClick = (selectedOption: ISelectedOption, record: any) => {
        const ganttInstance = ganttRef.current?.instance;
        const options = mapApiDataToDropdownOptions(ganttInstance.project.resources.map(model => model.data));
        props.openModal(<OwnerCoOwnerModal
            onUpdateTaskOwnerCoOwner={async (param: any) => {
                updateTaskAssignments(record, [param], true);
            }}
            taskId={record.id}
            projectGuid={props.projectGuid}
            webpartContext={props.webpartContext}
            options={options}
            selectedOption={selectedOption} isOwner={true} />)
    }
Attachments
Bryntum owner.mp4
(13.28 MiB) Downloaded 17 times

Post by alex.l »

Hi,

You refer to components that you didn't provide, such as UserPersona and ActionButton
Not sure what's the code do, assignments already have a link to resource record.

We understand your problem and believe you are able to reproduce it, but to confirm a bug or to help you if it's a problem on your side, we have to have an option to debug and reproduce it.

Pretty hard to guess without runnable test case and debugging. Any chance to get it?
And please try to remove React cmp from the column renderer, try to place simple text and see if the problem in component re-render or in column renderer itself?

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy


Post by ashishtakeda »

Hello Alex,

Please don't consider the UserPersona (to display owner image+name) and ActionButton(to open a modal) for now. we don't have bug there at all, we just wanted to know if i want to replace the current cell value with the new value what we need to do?

I have provided a video in my last reply, a runnable code to make you understand the requirement.

Suppose i have a column called Owner, inside cell i am rendering the owner details like name (e.g "Alex") or some react component, also i have button in the same cell (using renderer method) to change the name, on click on the button i want the cell would show the new name "Ashish", now tell me how to do that, forget about component or anything else, just tell me how to render the new value in the same cell? Please consider the name which renders inside the cell is coming from record.assignment and record.resources (please check the code), and on button click i want to change the name ("Alex" to "Ashish"), what to do just tell me.


Post by alex.l »

Hi,

The thing is that the cell content does update with no need in extra actions. And if you remove React component from cell and only return HTML (that I asked to test), you will see it worked as expected. SO the problem in React component life cycle, it prevents from updates. I will ask team if we have any suggestions for a workaround or I will file a ticket for that.

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy


Post by ashishtakeda »

Hello Alex,

I think there is a confusion (not sure what's exactly, can you tell me what's your understanding is), i don't want to use the double click functionality to change the cell value, i want when user clicks on the button then change the value of that cell, it's pretty basic thing, no? As you suggest i have removed everything now, see the below code, on click of "handleOwnerClick" i want to change the old value to new value, can you tell me how to do that? Screenshot attached.

To update the new cell value we have to write some code, some updater function or something right? i don't want to use cell editor functionality (double click)i want on click of the button the cell value should change, you understand what i mean?

{
                            id: 'owner',
                            type: 'column',
                            text: 'Owner',
                            width: 120,
                            editor: false,
                           
renderer: ({ record, grid }) => { let owner = undefined; if (record.assignments.length > 0) { const resourceMap = new Map(record.resources.map(r => [r.id, r])); for (const assignment of record.assignments) { const resource = resourceMap.get(assignment.resourceId); if (!resource) continue; if (assignment.isOwner) { owner = resource; } } } return <div className="d-flex"> {owner ? <span>{owner.name}</span> : ''} <button onClick={() => handleOwnerClick(owner, record)}>{"change name"}</button> </div> } },
const handleOwnerClick = (owner, record) => {
//suppose i am getting value here as Alex
const newVal = "Alex"
//tell me the logic to update the new cell value ("Alex") programmatically into that cell
}
Attachments
Screenshot 2025-11-11 120049.png
Screenshot 2025-11-11 120049.png (20.39 KiB) Viewed 998 times

Post by alex.l »

Hi,

Please see https://bryntum.com/products/grid/docs/api/Grid/column/WidgetColumn
We have demo here with WidgetColumn example https://bryntum.com/products/grid/examples/widgetcolumn/

To change value in a cell you need to update value of a record whom values are shown in that row.

To change assignment or a task you can use https://bryntum.com/products/gantt/docs/api/Gantt/model/TaskModel#function-assign
https://bryntum.com/products/gantt/docs/api/Gantt/model/TaskModel#function-unassign

How to get record in button handler you can see here

const grid = new Grid({
    appendTo : targetElement,

// makes grid as high as it needs to be to fit rows
autoHeight : true,
store      : {
    fields : ['price', 'inflightMeal', 'priorityBoarding'],
    data   : DataGenerator.generateData(5).map(data => {
        data.price = Math.round(Math.random() * 1000) + 100;
        return data;
    })
},
rowHeight : 100,
columns   : [
    { field : 'city', text : 'Destination', flex : 1 },
    {
        type    : 'widget',
        text    : 'Extras',
        align   : 'center',
        width   : 300,
        widgets : [
            {
                type  : 'checkbox',
                name  : 'inflightMeal',
                label : 'In-flight Meal'
            },
            {
                type  : 'checkbox',
                name  : 'priorityBoarding',
                label : 'Priority boarding'
            }
        ]
    },
    {
        text       : 'Total price',
        align      : 'right',
        htmlEncode : false,
        renderer({ record }) {
            let total = record.price;

            if (record.inflightMeal) {
                total += 30;
            }
            if (record.priorityBoarding) {
                total += 50;
            }

            return `<strong>$ ${total}</strong>`;
        }
    },
    {
        type    : 'widget',
        text    : 'Button column',
        width   : 140,
        widgets : [{
            type    : 'button',
            cls     : 'b-raised',
            icon    : 'b-icon b-fa-plane',
            text    : 'Book now',
            flex    : 1,
            // 'up.' will find the implementation in the Grid
            onClick : 'up.bookFlight'
        }]
    }
],

bookFlight({ source : button }) {
    // The cell's widget has a cellInfo context block
    const { record } = button.cellInfo;

    Toast.show(
        `Booking a flight to ${record.city}.` +
        (record.inflightMeal ? ' Meal: \u2713' : '') +
        (record.priorityBoarding ? ' Priority: \u2713' : '')
    );
}
});

All the best,
Alex Lazarev

How to ask for help? Please read our Support Policy


Post Reply