Our state of the art Gantt chart


Post by revill »

Hi,
I need guidance from Your side. I'm trying to create a Salesforce LWC component with the Gannt chart. I want to include there group-by and text-filtering functionalities.
Both functionalities are working as expected independently, however, when data is grouped by, the text filtering is not working. It is working properly when Tasks are not grouped. Please, let me know if maybe I'm missing something important here?

Please, find my code below:

/* globals bryntum : true */
import { LightningElement } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadScript, loadStyle } from "lightning/platformResourceLoader";
import GANTT from "@salesforce/resourceUrl/bryntum_gantt";
import GanttToolbarMixin from "./lib/GanttToolbar";
import data from './data/launch-saas'

export default class Gantt_component extends LightningElement {
    error;
    groupBySet = ['Country'];
    currentGroupBy = 'None';
    currentSearchPhrase;


projects = [
    {
       "Id":"a0i7Q000000ctE3QAI",
       "Name":"Diksmuide",
       "sitetracker__Project_Start_Date_A__c":"2020-01-01",
       "Start_of_construction_A__c":"2023-02-01",
        "Country": "BEL",
        "Site": "Diksmuide"
    },
    {
       "Id":"a0i7Q000000ctE4QAI",
       "Name":"Noord",
       "sitetracker__Project_Start_Date_A__c":"2020-01-01",
       "Start_of_construction_A__c":"2023-05-05",
       "Country": "NL",
       "Site": "Noord"
    },
    {
       "Id":"a0i7Q000000ctEhQAI",
       "Name":"Ijzer",
       "sitetracker__Project_Start_Date_A__c":"2019-07-08",
       "Start_of_construction_A__c":"2023-08-01",
       "Country": "USA",
       "Site": "Ijzer"
    }
 ];

renderedCallback() {
    if (this.bryntumInitialized) {
        return;
    }
    this.bryntumInitialized = true;

Promise.all([
    loadScript(this, GANTT + "/gantt.lwc.module.js"),
    loadStyle(this, GANTT + "/gantt.stockholm.css")
    ])
    .then(() => {
        try {
            this.handleProjectLoad();

    } catch (error) {
        console.log('error ', error);
    }
})
.catch(error => {
    this.dispatchEvent(
        new ShowToastEvent({
            title: "Error loading Bryntum Gantt",
            message: error,
            variant: "error"
        })
    );
});
}

createProjectRow(element, projectStartDate, projectEndDate, segments, earliestStartDate, idNumber) {
    let projectRow;
    projectRow = {"id" : idNumber, "manuallyScheduled" : true, "startDate" : String (projectStartDate), "endDate" : String (projectEndDate), "name" : element.Name, "salesforceId" : element.Id, "Country" : element.Country, "Site" : element.Site};
    return projectRow;
}

handleProjectLoad() {
    let projectTasks = new Array;
    let idNumber = 1;
    let earliestStartDate = new Date();
    for (const element of this.projects) {
        let projectStartDate =  element.sitetracker__Project_Start_Date_A__c;
        let projectEndDate =  element.Start_of_construction_A__c;
        if (projectEndDate != undefined && projectEndDate != null && projectStartDate != undefined && projectStartDate < projectEndDate) {
            let segments = [];
            let sortedSegments = segments.sort(function(a,b){
                return new Date(a.startDate) - new Date(b.startDate);
            });
            let newstartEventDate = new Date(projectEndDate);
            let endEventDate = new Date(projectEndDate);
            newstartEventDate.setDate(endEventDate.getDate() -1);

    let projectRow = this.createProjectRow(element, projectStartDate, projectEndDate, sortedSegments, earliestStartDate, idNumber);
    projectTasks.push(projectRow);
    idNumber++;
}
}

this.createGantt(projectTasks);
}

createGantt(tasks) {
    const GanttToolbar = GanttToolbarMixin(bryntum.gantt.Toolbar);

const project = new bryntum.gantt.ProjectModel({
    calendar: {
        "calendar"  : "general"
    },
    tasksData: tasks,
    calendarsData: data.calendars.rows
});

const gantt = new bryntum.gantt.Gantt({
    project,
    appendTo: this.template.querySelector(".container"),
    startDate: "2019-01-12",
    endDate: "2019-03-24",

tbar: {
    items :[
        {
            type: 'buttonGroup',
            items : [
                {
                    type : 'button',
                    text : '<<',
                    onClick : (event) => {
                        const collapseEvent = event.source.up ('gantt');
                        collapseEvent.shiftPrevious(); 
                    }
                },
                {
                    type : 'button',
                    text : '>>',
                    onClick : (event) => {
                        const collapseEvent = event.source.up ('gantt');
                        collapseEvent.shiftNext(); 
                    }
                },
                {
                    type : 'button',
                    text : 'Zoom to fit',
                    onClick : (event) => {
                        const collapseEvent = event.source.up ('gantt');
                        collapseEvent.zoomToFit(); 
                    }
                }
            ]
        },
    {
        type        : 'combo',
        ref         : 'groupBy',
        label       : 'Group by',
        placeholder : 'No grouping',
        labelPosition : 'above',
        clearable   : true,
        inputWidth  : '7em',
        items       : Array.from(this.groupBySet),
        value : this.currentGroupBy,
        listeners : {
            change : (event) => {
                console.log('group by change', event.value);
                this.currentGroupBy = event.value;
                if (this.currentGroupBy === 'None') {
                    gantt.clearGroups();
                } else {
                    switch (event.value) {
                        case 'Country' :
                            console.log('country change');
                            gantt.group(['Country']);
                            break;
                        case 'Program' :
                            gantt.group(['Program']);
                            break;
                        case 'Project Template' :
                            gantt.group(['ProjectTemplate']);
                            break;
                        case 'Construction Budget Code' : 
                            gantt.group(['ConstructionBudget']);
                            break;
                        case 'GC Budget Code' : 
                            gantt.group(['GCBudgetCode']);
                            break;
                        default :
                            gantt.clearGroups();
                            break;
                    }
                }
            }
        }
    },
    {
        label : 'Search By Site',
        labelPosition : 'above',
        type : 'textfield',
        placeholder : 'Search By Site',
        value : this.currentSearchPhrase,
        listeners : {
            input(event) {
                console.log('input event', event);
                const gantt = event.source.up('gantt');
                const value = event.value;
                this.currentSearchPhrase = value;
                if (value === '') {
                    gantt.project.eventStore.clearFilters();
                } else {
                    gantt.project.eventStore.clearFilters();
                    gantt.project.eventStore.filter((item) => {
                        console.log('input item', item.originalData);
                        console.log('input item', item.originalData.Country);

                        this.currentSearchPhrase = value;

                        return item.originalData.Site.startsWith(value);
                    });
                }
            },
            change(event) { console.log('change event', event);
                const gantt = event.source.up('gantt');
                const value = event.value;
                if (value === '') {
                    gantt.project.eventStore.clearFilters();
                } else {
                    gantt.project.eventStore.clearFilters();
                    gantt.project.eventStore.filter((item) => {
                        return item.originalData.Site.startsWith(value);
                    });
                }
            }

        }
    }
]
},

dependencyIdField: "sequenceNumber",
columns: [
    { type: "name", width: 250 },
    { type: "startdate", field : 'startDate', format : 'YYYY-MM-DD'},
    { type: "enddate",  field : 'endDate', format : 'YYYY-MM-DD'},
    { text  : "Country", hidden : false, field : 'Country'},
    { type: "addnew" }
],

subGridConfigs: {
    locked: {
        flex: 3
    },
    normal: {
        flex: 4
    }
},

columnLines: false,

features: {
    rollups: {
        disabled: true
    },
    baselines: {
        disabled: true
    },
    progressLine: {
        disabled: true,
        statusDate: new Date(2019, 0, 25)
    },
    filter: true,
    dependencyEdit: true,
    timeRanges: {
        showCurrentTimeLine: true
    },
    labels: {
        left: {
            field: "name",
            editor: {
                type: "textfield"
            }
        }
    },
    treeGroup : {
        levels : ['Country']
    }
}
});

project.load();

project.commitAsync().then(() => {
    // console.timeEnd("load data");
    const stm = gantt.project.stm;

stm.enable();
stm.autoRecord = true;

// let's track scheduling conflicts happened
project.on("schedulingconflict", context => {
    // show notification to user
    bryntum.gantt.Toast.show(
        "Scheduling conflict has happened ..recent changes were reverted"
    );
    // as the conflict resolution approach let's simply cancel the changes
    context.continueWithResolutionResult(
        bryntum.gantt.EffectResolutionResult.Cancel
    );
});
});
}
}

Thank you in advance!


Post by alex.l »

Thank you for the report. TreeGroup feature have some restrictions, but that should support filtering.

I've opened a ticket to fix this: https://github.com/bryntum/support/issues/6110

All the best,
Alex


Post by revill »

Hi Alex,
Thanks for the info. Are you able to let me know more or less when the bugfix will be available?

Best regards


Post by alex.l »

Hi,

There is an update. When TreeGroup feature enabled, please use gantt.store instead of gantt.treeStore for interaction. This feature creates new store with new data structure for displaying. We will update docs with this information.

Thank you!

All the best,
Alex


Post by revill »

Hi Alex,
That's great. I didn't expect so a quick fix, great job!
However, I'm not sure if I understand correctly. Does it mean that I should define a new Store for the Gantt if I want to use the grouping functionality?
So based on my example, should I do something like that?

const project = new bryntum.gantt.ProjectModel({
    calendar: {
        "calendar"  : "general"
    },
    tasksData: tasks,
    calendarsData: data.calendars.rows
});

const gantt = new bryntum.gantt.Gantt({
    project,
    store : new bryntum.gantt.Store({
                data: tasks
                            }),    
appendTo: this.template.querySelector(".container"), startDate: "2019-01-12", endDate: "2019-03-24", tbar: { items :[ { type: 'buttonGroup', items : [ { type : 'button', text : '<<', onClick : (event) => { const collapseEvent = event.source.up ('gantt'); collapseEvent.shiftPrevious(); } }, { type : 'button', text : '>>', onClick : (event) => { const collapseEvent = event.source.up ('gantt'); collapseEvent.shiftNext(); } }, { type : 'button', text : 'Zoom to fit', onClick : (event) => { const collapseEvent = event.source.up ('gantt'); collapseEvent.zoomToFit(); } } ] }, { type : 'combo', ref : 'groupBy', label : 'Group by', placeholder : 'No grouping', labelPosition : 'above', clearable : true, inputWidth : '7em', items : Array.from(this.groupBySet), value : this.currentGroupBy, listeners : { change : (event) => { console.log('group by change', event.value); this.currentGroupBy = event.value; if (this.currentGroupBy === 'None') { gantt.clearGroups(); } else { switch (event.value) { case 'Country' : console.log('country change'); gantt.group(['Country']); break; case 'Program' : gantt.group(['Program']); break; case 'Project Template' : gantt.group(['ProjectTemplate']); break; case 'Construction Budget Code' : gantt.group(['ConstructionBudget']); break; case 'GC Budget Code' : gantt.group(['GCBudgetCode']); break; default : gantt.clearGroups(); break; } } } } }, { label : 'Search By Site', labelPosition : 'above', type : 'textfield', placeholder : 'Search By Site', value : this.currentSearchPhrase, listeners : { input(event) { console.log('input event', event); const gantt = event.source.up('gantt'); const value = event.value; this.currentSearchPhrase = value; if (value === '') { gantt.project.eventStore.clearFilters(); } else { gantt.project.eventStore.clearFilters(); gantt.project.eventStore.filter((item) => { console.log('input item', item.originalData); console.log('input item', item.originalData.Country); this.currentSearchPhrase = value; return item.originalData.Site.startsWith(value); }); } }, change(event) { console.log('change event', event); const gantt = event.source.up('gantt'); const value = event.value; if (value === '') { gantt.project.eventStore.clearFilters(); } else { gantt.project.eventStore.clearFilters(); gantt.project.eventStore.filter((item) => { return item.originalData.Site.startsWith(value); }); } } } } ] }, dependencyIdField: "sequenceNumber", columns: [ { type: "name", width: 250 }, { type: "startdate", field : 'startDate', format : 'YYYY-MM-DD'}, { type: "enddate", field : 'endDate', format : 'YYYY-MM-DD'}, { text : "Country", hidden : false, field : 'Country'}, { type: "addnew" } ], subGridConfigs: { locked: { flex: 3 }, normal: { flex: 4 } }, columnLines: false, features: { rollups: { disabled: true }, baselines: { disabled: true }, progressLine: { disabled: true, statusDate: new Date(2019, 0, 25) }, filter: true, dependencyEdit: true, timeRanges: { showCurrentTimeLine: true }, labels: { left: { field: "name", editor: { type: "textfield" } } }, treeGroup : { levels : ['Country'] } } });

Post by alex.l »

Nope, you can use it as you did before, but for filtering you need to replace gantt.project.eventStore. to gantt.store and apply filters on it.

All the best,
Alex


Post by revill »

Thanks a lot, Alex. It's working as expected :D


Post Reply