Hi,
I'm trying to achieve similar functionality as it is in the https://www.bryntum.com/products/gantt/examples/split-tasks/
In my Salesforce project, I would like to display the phases of the task in one row, not in separate rows.
Based on the example mentioned above I have added a segments array to my Task structure which looks like that right now:
but I'm getting an error that I don't know how to solve:
Uncaught (in promise) TypeError: Class constructor N cannot be invoked without 'new'
at EF.taskRenderer (gantt_component.js:1:25451)
at EB.internalPopulateTaskRenderData (gantt.lwc.module.js:53:1655986)
at Aq.generateSegmentRenderData (gantt.lwc.module.js:53:1247230)
at eval (gantt.lwc.module.js:53:1248426)
at Array.map (<anonymous>)
at Aq.appendDOMConfig (gantt.lwc.module.js:53:1248388)
at Aq.onTaskDataGenerated (gantt.lwc.module.js:53:1249284)
at functionChainRunner (gantt.lwc.module.js:10:903135)
at EM.<computed> [as onTaskDataGenerated] (gantt.lwc.module.js:10:902667)
at EB.populateTaskRenderData (gantt.lwc.module.js:53:1657572)
If I'm trying to build a segments array in a loop using fields from a different object it is not working and it just displays the whole project bar in the row
for ( const phase of phases) {
let forecastFieldStartField = this.checkActualForecastField(phase.Start_Date_Field__c,element);
let forecastFieldEndField = this.checkActualForecastField(phase.End_Date_Field__c,element);
let startDate = element[phase.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[phase.Start_Date_Field__c];
let endDate = element[phase.End_Date_Field__c] == undefined ? element[forecastFieldEndField] : element[phase.End_Date_Field__c];
if (endDate != undefined && endDate != null) {
segments.push({"name" : phase.Name, "startDate" : startDate, "endDate" : endDate});
}
}
but if I add to this array segments created manually as it is in the first part of the code, it's displaying splited tasks in the Gantt chart.
I'm struggling with this issue for a longer time and right now I don't have any more ideas on what to do, to make it work with the segments created in the loop with the data taken from different object.
I tried to do that, but originally it uses data from the Salesforce org, but when I'm trying to use mock data defined inside the component to provide a working example, it is working properly as expected and Project is splitted into parts.
In the meantime, I found out that when I'm adding segments defined in the loop, there is some recalculation of the endDate of the project which is not the expected end date. I have set in the projectRow attribute "manuallyScheduled" as true, but it seems to not work when the segments created dynamically are added. If segments are written manually, then recalculation is not executed and task splitting works as expected.
I'm not sure, but it looks to me like there is some problem with the date format when it's taken from the external object. I tried to pass it to the segment definition as a string or as a Date object, but it still was not working properly for me.
We need a code example to help you with that. We need to reproduce that for debugging.
I found out that when I'm adding segments defined in the loop, there is some recalculation of the endDate of the project which is not the expected end date.
Can't you collect segments and set them all at once outside of your loop?
Hi Alex,
I'm collecting segments in the loops and later in the main loop for a project I'm adding a segment array for each project row.
Please, find the code below. In this version, splitting task is not working properly. But if I add commented line nr 289 it is working properly.
/* globals bryntum : true */
import { LightningElement, api, wire } 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'
import getProjects from '@salesforce/apex/GanttChartApexHandler.getProjects';
import getGanttChartConfigurationsWrapper from '@salesforce/apex/GanttChartApexHandler.getGanttChartConfigurationsWrapper'
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import PHASE_EVENT_CONFIG_OBJECT from '@salesforce/schema/Phase_Event_Gantt_Chart__c';
export default class Gantt_component extends LightningElement {
projects;
error;
hideCountryColumn = true;
hideFullSiteName = true;
hideProjectTemplate = true;
hideGCBudgetCodeCol = true;
hideConstructionBudgetCol = true;
hideProjectStatusCol = true;
configurationList = [];
currentConfigurationUsed;
currentconfigPhasesAndEvents;
configurationNames = [];
mapOfConfigurationsByname = new Map();
@wire(getObjectInfo, { objectApiName: PHASE_EVENT_CONFIG_OBJECT })
phaseEventObjectInfo;
get recordTypeId() {
// Returns a map of record type Ids
const rtis = this.phaseEventObjectInfo.data.recordTypeInfos;
console.log('recordTypeId map : ',rtis );
return Object.keys(rtis).find(rti => rtis[rti].name === 'Phase');
}
renderedCallback() {
if (this.bryntumInitialized) {
return;
}
this.bryntumInitialized = true;
Promise.all([
loadScript(this, GANTT + "/gantt.lwc.module.js"),
loadStyle(this, GANTT + "/gantt.stockholm.css")
])
.then(() => {
this.handleConfigurations();
this.handleProjectLoad();
})
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: "Error loading Bryntum Gantt",
message: error,
variant: "error"
})
);
});
}
createGantt(tasks, startDate) {
console.log('tasks', tasks);
const GanttToolbar = GanttToolbarMixin(bryntum.gantt.Toolbar);
const project = new bryntum.gantt.ProjectModel({
startDate : startDate,
calendar: data.project.calendar,
tasksData: tasks
});
let columns = [
{ type: "name", width: 250 },
{ type: "startdate" },
{ type: "enddate", field : 'endDate' },
{ type: "duration" },
{ text : "country", hidden : false, field : 'Country' },
{ text : "Full Site Name", hidden : this.hideFullSiteName, field : 'FullSiteName'},
{ text : "Project Template" , hidden : this.hideProjectTemplate, field : 'ProjectTemplate'},
{ text : "GC Budget Code", hidden : this.hideGCBudgetCodeCol, field : 'GCBudgetCode'},
{ text : "Construction Budet Code", hidden : this.hideConstructionBudgetCol, field : 'ConstructionBudget'},
{ text : "Project Status" , hidden : this.hideProjectStatusCol, field : 'ProjectStatus' },
{ type: "addnew" }
];
const gantt = new bryntum.gantt.Gantt({
project,
appendTo: this.template.querySelector(".container"),
startDate: startDate,
cls : 'custom-styling',
taskRenderer({ taskRecord }) {
console.log('Test taskRenderer isEventSegment ', taskRecord.isEventSegment + ' ' + taskRecord.name);
// Display segment names
if (taskRecord.isEventSegment) {
return bryntum.gantt.StringHelper.encodeHtml(taskRecord.name);
}
return '';
},
columns: columns,
subGridConfigs: {
locked: {
flex: 3
},
normal: {
flex: 4
}
},
tbar : [
{
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 : 'button',
text : '+',
onClick : (event) => {
const collapseEvent = event.source.up ('gantt');
collapseEvent.zoomIn();
}
},{
type : 'button',
text : '-',
onClick : (event) => {
const collapseEvent = event.source.up ('gantt');
collapseEvent.zoomOut();
}
}
],
columnLines: false,
features: {
cellEdit : false,
taskEdit : false,
taskMenu : false,
taskDrag : false,
taskResize : false,
taskDragCreate : false,
rollups: {
disabled: true
},
baselines: {
disabled: true
},
progressLine: {
disabled: true,
statusDate: new Date(2019, 0, 25)
},
criticalPaths : false,
filter: true,
dependencyEdit: true,
timeRanges: {
showCurrentTimeLine: true
},
labels: {
left: {
field: "name",
editor: {
type: "textfield"
}
}
}
}
});
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
);
});
});
}
handleProjectLoad() {
getProjects()
.then((result) => {
let phases = [];
let events = [];
console.log('test this.currentconfigPhasesAndEvents ',this.currentconfigPhasesAndEvents);
this.currentconfigPhasesAndEvents.forEach (element => {
if (element.RecordTypeId == this.recordTypeId) {
phases.push (element);
} else {
events.push (element);
}
})
this.projects = result;
this.error = undefined;
let projectTasks = new Array;
let idNumber = 1;
let earliestStartDate = new Date();
console.log('test this.projects ',this.projects);
this.projects.forEach(element => {
let segments = [];
for ( const phase of phases) {
if ((phase.Name === 'Grid Connection' || phase.Name === 'Building Permit') && (!element.Grid_Connection_required__c || !element.Building_Permit_required__c) ) {
continue;
}
let forecastFieldStartField = this.checkActualForecastField(phase.Start_Date_Field__c,element);
let forecastFieldEndField = this.checkActualForecastField(phase.End_Date_Field__c,element);
let startDate = element[phase.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[phase.Start_Date_Field__c];
let endDate = element[phase.End_Date_Field__c] == undefined ? element[forecastFieldEndField] : element[phase.End_Date_Field__c];
if (endDate != undefined && endDate != null) {
let parsedDate = new Date(startDate);
if (parsedDate.getMonth() == '0' && parsedDate.getDate() == '1' && parsedDate.getFullYear() == '1900') {
console.log('continue parsed date parsedDate: ',parsedDate.getDate());
continue;
}
let phaseEndDate = new Date(endDate);
let phaseStartDate = new Date(startDate);
let phaseOneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
let durationPhase = 0;
if (phaseEndDate !== undefined && phaseStartDate !== undefined) {
durationPhase = Math.round(Math.abs((phaseEndDate.getTime() - phaseStartDate.getTime()) / phaseOneDay));
}
segments.push({"name" : phase.Name, "duration" : durationPhase, "startDate" : phaseStartDate, "endDate" : phaseEndDate});
}
}
for (const event of events) {
let forecastFieldStartField = this.checkActualForecastField(event.Start_Date_Field__c,element);
let startDate = element[event.Start_Date_Field__c] == undefined ? element[forecastFieldStartField] : element[event.Start_Date_Field__c];
if (startDate != undefined && startDate != null) {
let parsedDate = new Date(startDate);
if (parsedDate.getMonth() == '0' && parsedDate.getDate() == '1' && parsedDate.getFullYear() == '1900') {
console.log('continue parsed date parsedDate: ',parsedDate.getDate());
continue;
}
let endEventDate = new Date(startDate);
let newstartEventDate = new Date(startDate);
endEventDate.setDate(newstartEventDate.getDate() + 1);
segments.push({"name" : event.Name, "startDate" : newstartEventDate, "duration" : 1});
}
}
let projectStartDate = element.sitetracker__Project_Start_Date_A__c == undefined ? element.sitetracker__Project_Start_Date_F__c : element.sitetracker__Project_Start_Date_A__c;
let projectEndDate = element.Start_of_construction_A__c == undefined ? element.Start_of_construction_F__c : element.Start_of_construction_A__c;
const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
let duration = 0;
if (projectEndDate !== undefined && projectStartDate !== undefined) {
duration = Math.round(Math.abs((new Date(projectEndDate).getTime() - new Date(projectStartDate).getTime()) / oneDay));
}
if (projectEndDate != undefined && projectEndDate != null && projectStartDate != undefined) {
let parsedDate = new Date(projectStartDate);
let parsedEndDate = new Date(projectEndDate);
let projectRow;
// segments.push({"name" : "DDOs", "startDate" : "2022-01-14", "endDate" : "2022-03-18"},{"name" : "Ports", "startDate" : "2023-01-21", "endDate" : "2023-06-22"});
if (parsedDate.getDate() =='1' && parsedDate.getMonth() == '0' && parsedDate.getFullYear() == '1900') {
projectRow = {"id" : idNumber,"manuallyScheduled": true, autoCalculateEndDate : false, "duration" : duration, "segments" : segments, "name" : element.sitetracker__Site__r.Name, "endDate" : parsedEndDate, "salesforceId" : element.Id, "ProjectTemplate" : element.sitetracker__Project_Template__c, "Country" : element.Country__c, "ProjectTemplate" : element.sitetracker__Project_Template__c, "GCBudgetCode" : element.GC_Budget_Code__c, "ConstructionBudget" : element.Construction_Budget_Code__c, "ProjectStatus" : element.sitetracker__Project_Status__c};
} else {
if (parsedDate < earliestStartDate) {
earliestStartDate = parsedDate;
}
projectRow = {"endDate" : parsedEndDate, autoCalculateEndDate : false, "id" : idNumber, "duration" : duration, "manuallyScheduled": true, "name" : element.sitetracker__Site__r.Name, "segments" : segments, "startDate" : parsedDate };//, "children" : children};
}
projectTasks.push(projectRow);
idNumber++;
}
});
this.createGantt(projectTasks, earliestStartDate);
})
.catch((error) => {
this.error = error;
console.log('ERROR: ', error);
this.projects = undefined;
});
};
checkActualForecastField (fieldActual, object) {
const sufix = 'F__c';
if (fieldActual != null && fieldActual != undefined && object != null && object != undefined) {
fieldActual += '';
let fieldNameWithoutSufix = fieldActual.substring(0, fieldActual.length-4);
console.log('fild name without sufx ', fieldNameWithoutSufix);
return fieldNameWithoutSufix.concat(sufix);
}
};
handleConfigurations() {
let configwrapper;
getGanttChartConfigurationsWrapper()
.then((configWrapper) => {
let mainConfigObjects = configWrapper.configurations;
this.configurationList = configWrapper.configurations;
if (this.configurationNames?.length === 0 || this.mapOfConfigurationsByname.length === 0) {
console.log('test vconfigWrapper.configurations ',configWrapper.configurations);
configWrapper.configurations.forEach(element => {
if (element?.Name != undefined) {
this.configurationNames.push(element.Name);
this.mapOfConfigurationsByname.set(element.Name, element);
}
})
this.currentConfigurationUsed = mainConfigObjects[0];
}
this.currentconfigPhasesAndEvents = configWrapper.phaseEvents;
let fieldsToBeDisplayed = this.currentConfigurationUsed.Display_Fields__c;
let groupByField = this.currentConfigurationUsed.Grouping_By__c;
})
.catch((error) => {
console.log('error: ', error);
this.error = error;
this.configWrapper = undefined;
});
}
}
I have tried to mock the project's data, but when I'm mocking a structure like that:
this.projects = [{"sitetracker__Site__r" : {Name: 'mock1', Id: 'a0p7Q000000YWVWQA4'},
"Opening_of_Site_A" : new Date ("2023-05-12"),
"Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
"Building_Permit_Approved_A" : new Date ("2022-03-25"),
"Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
"Contract_signed_by_Fastned_A" : new Date ("2021-10-22"), "sitetracker__Project_Start_Date_A__c" : new Date ("2021-03-11"), "Start_of_construction_A__c" : new Date ("2023-04-09")},{"sitetracker__Site__r" : {Name: 'mock2', Id: 'a0p7Q000000YWVWQA4'},"Opening_of_Site_A" : new Date ("2023-05-12"),
"Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
"Building_Permit_Approved_A" : new Date ("2022-03-25"),
"Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
"sitetracker__Project_Start_Date_A__c" : new Date ("2021-03-11"), "Start_of_construction_A__c" : new Date ("2023-04-09")},{"sitetracker__Site__r" : {Name: 'mock3', Id: 'a0p7Q000000YWVWQA4'},"Opening_of_Site_A" : new Date ("2023-05-12"),
"Hand_in_final_Building_Permit_A" : new Date ("2022-01-24"),
"Building_Permit_Approved_A" : new Date ("2022-03-25"),
"Location_Agreement_signed_by_all_A" : new Date ("2022-03-25"),
"Contract_signed_by_Fastned_A" : new Date ("2021-10-22"), "sitetracker__Project_Start_Date_A__c" : new Date ("2022-03-11"), "Start_of_construction_A__c" : new Date ("2024-04-09")}];
I cannot reproduce the same behavior as I have when I use data from the Salesforce org. With mock data, it is splitted correctly.
Maybe the problem in data format then? Looks like when you mock data, you use that you expect to have, but in real app it's different.
Try to put a breakpoint and check what's actually came from the server? First of all, If it's Date objects or some formatted strings that cannot be parsed correctly.
I thought so, but I'm not able to find the issue. I have checked the date format and it's retrieved from server as a string in the format "yyyy-mm-dd". I was trying to pass it like that, or parse it to a Date object and it still doesn't work.
When I'm checking it in the debugger both version looks exactly the same. Here first is with data from server only which is not working:
And here are added just 2 segments manually and with those it is working:
I have no idea why. And why adding this two segments created manually is causing that the endDate recalculation is working again :/
We analysed the information provided. Unfortunately, we are out of ideas now. We need to see dataset you used to replicate the problem and debug it. Could you please attach JSON here and provide steps to reproduce?