Hello Bryntum Team,
I've been having issues between our custom task model object, transforming the flat data, and the two parent fields - parentId and parentIndex - not working correctly in conjunction with the dataSource field mapping.
First, regarding the "transformFlatData" flag and the "id" and "parentId" fields is not working correctly on the custom task model.
In my example, posted below, my CustomTaskModel has two fields - "jobId" and "containerId" - that I am mapping the datasources to the base task model fields "id" and "parentId". This does not render the correct parent-child relationship in the grid.
Here is my process:
If I remove the "containerId" field mapping and change the field name in the data from "containerId" to "parentId", I see an id collision error in the console, which I don't understand.
If I change the "id" field to "jobId" and set it using the "idField()" get function, the id collision disappears, but the data is still rendered flat.
It's not until I change the variable name in the data from "containerId" to "parentId" does it actually render correctly.
It seems as though the mapping of datasource to field is not working correctly, unless I misunderstand it. Or maybe there should be an overriding function similar to "idField()" but for the parentId field.
// import { Gantt, Column, ColumnStore, TaskModel, Model, Store, ProjectModel, StringHelper } ...
const _tasks = [{
jobId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
containerId: null,
title: 'Parent Task',
startDate: '2025-01-25T08:00:00.000Z',
endDate: '2025-02-08T08:00:00.000Z',
duration: 14,
durationUnit: 'day'
},
{
jobId: '06e6879f-de2b-4726-b9f9-f36deff05c4b',
containerId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
title: 'Child 1',
startDate: '2025-01-31T08:00:00.000Z',
endDate: '2025-01-31T08:00:00.000Z',
duration: 0,
durationUnit: 'day',
domainId: '201'
},
{
jobId: '07a32003-9080-4f8e-9d12-27d0df095b1a',
containerId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
title: 'Child 2',
startDate: '2025-01-30T08:00:00.000Z',
endDate: '2025-02-02T08:00:00.000Z',
duration: 3,
durationUnit: 'day',
domainId: '202'
},
{
jobId: '0a0f3630-ee49-4387-b5dc-623104e72380',
containerId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
title: 'Child 3',
startDate: '2025-01-29T08:00:00.000Z',
endDate: '2025-01-30T08:00:00.000Z',
duration: 1,
durationUnit: 'day',
domainId: '202'
}];
class CustomTaskModel extends TaskModel {
static $name = 'CustomTaskModel';
static get fields() {
return [
{ name: 'id', dataSource: 'jobId' },
{ name: 'parentId', dataSource: 'containerId' },
{ name: 'name', dataSource: 'title' },
{ name: 'domainId', dataSource: 'domainId' },
];
}
static get idField() {
return 'id';
}
}
class DomainModel extends Model {
static $name = 'DomainModel';
static get fields() {
return [
{ name: 'domainId', dataSource: 'domainId' },
{ name: 'name', dataSource: 'label' },
];
}
static get idField() {
return 'domainId';
}
}
const domainStore = new Store({
id: 'domains',
modelClass: DomainModel
});
class DomainColumn extends Column {
static $name = 'DomainColumn';
static type = 'domainColumn';
static get isGanttColumn() {
return true;
}
static get defaults() {
return {
text: 'Domain',
field: 'domainId',
renderer({ value }) {
const domain = domainStore.getById(value);
return domain ? domain.name : '';
},
editor: {
displayField: 'name',
valueField: 'domainId',
type: 'combo',
store: domainStore,
},
};
}
}
// Register the column type for use below
ColumnStore.registerColumnType(DomainColumn);
const project = new ProjectModel({
loadUrl : '../_datasets/launch-saas.json',
syncUrl : '/pretendSyncUrl',
autoLoad : true,
autoSync : true,
autoSetConstraints : true,
taskModelClass: CustomTaskModel,
taskStore: {
// Set default transform flat tasks to task tree
transformFlatData: true,
// Enable WBS column to update every time a task order changes
wbsMode: 'auto',
},
listeners: {
beforeSend(event) {
console.log(`-> beforeSend:`);
console.log(event);
},
beforeLoad(event) {
console.log(`--> beforeLoad`);
// Adding the "domains" store to the request
event.pack.stores = ['calendars', 'dependencies', 'tasks', 'domains'];
},
beforeLoadApply(event) {
console.log(`--> beforeLoadApply`);
// Simulate the domain data in the response
event.response.domains = {
rows: [
{ domainId: '201', label: 'Software' },
{ domainId: '202', label: 'Hardware' }
]
};
// Override the task data
event.response.tasks.rows = _tasks;
},
beforeSync({ pack }) {
console.log(`--> beforeSync`);
console.log(pack);
},
}
});
// Add the custom store to the project
project.addCrudStore(domainStore);
new Gantt({
appendTo : 'container',
dependencyIdField : 'sequenceNumber',
rowHeight : 45,
tickSize : 45,
barMargin : 8,
project : project,
scrollTaskIntoViewOnCellClick : true,
selectionMode : {
cell : false,
dragSelect : true,
},
rowReorder: {
showGrip: true,
},
columns : [
{ type : 'wbs' },
{ type : 'name', width : 250 },
{ type : 'domainColumn', name: 'Domain', width : 250 }
],
features: {
taskEdit: {
items: {
generalTab: {
items: {
percentDone: false,
effort: false,
domain: {
type: 'combo',
flex: '1 1 100%',
label: 'Domain',
displayField: 'name',
valueField: 'domainId',
store: domainStore,
},
}
}
},
},
},
listeners: {
beforeTaskEditShow({ editor, taskRecord }) {
editor.widgetMap.domain.value = domainStore.getById(taskRecord.domainId);
},
},
});
Second, the "parentIndex" does not appear to be honored at the root level.
In this code sample, the tree is correctly built but the two items at the top level should be in the inverse order. At the child level, the parentIndex is working as expected; changing it in the data reorders the child tasks correctly.
I thought that perhaps that the top level parentId set to null was an issue but omitting the property had no effect either.
Here is a code sample for that:
// import { Gantt, Column, ColumnStore, TaskModel, Model, Store, ProjectModel, StringHelper } ...
const _tasks = [{
jobId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
parentId: null,
parentIndex: 1,
title: 'Parent Task',
startDate: '2025-01-25T08:00:00.000Z',
endDate: '2025-02-08T08:00:00.000Z',
duration: 14,
durationUnit: 'day'
},
{
jobId: '06e6879f-de2b-4726-b9f9-f36deff05c4b',
parentId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
parentIndex: 1,
title: 'Child 1',
startDate: '2025-01-31T08:00:00.000Z',
endDate: '2025-01-31T08:00:00.000Z',
duration: 0,
durationUnit: 'day',
domainId: '201'
},
{
jobId: '07a32003-9080-4f8e-9d12-27d0df095b1a',
parentId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
parentIndex: 2,
title: 'Child 2',
startDate: '2025-01-30T08:00:00.000Z',
endDate: '2025-02-02T08:00:00.000Z',
duration: 3,
durationUnit: 'day',
domainId: '202'
},
{
jobId: '0a0f3630-ee49-4387-b5dc-623104e72380',
parentId: 'bb9cd4bc-e0a0-42dd-8730-380f7d1a6c18',
parentIndex: 0,
title: 'Child 0',
startDate: '2025-01-29T08:00:00.000Z',
endDate: '2025-01-30T08:00:00.000Z',
duration: 1,
durationUnit: 'day',
domainId: '202'
},
{
jobId: '3b10cdb7-bdce-4dab-96bb-240ebd442a3b',
parentId: null,
parentIndex: 0,
title: 'This should be the FIRST Task',
description: null,
startDate: '2025-01-17T08:00:00.000Z',
endDate: '2025-01-20T08:00:00.000Z',
duration: 3,
durationUnit: 'day',
}];
class CustomTaskModel extends TaskModel {
static $name = 'CustomTaskModel';
static get fields() {
return [
{ name: 'jobId', dataSource: 'jobId' },
// These don't appear to work correctly
// { name: 'parentId', dataSource: 'containerId' },
// { name: 'parentIndex', dataSource: 'sortOrder' },
{ name: 'name', dataSource: 'title' },
{ name: 'domainId', dataSource: 'domainId' },
];
}
static get idField() {
return 'jobId';
}
}
class DomainModel extends Model {
static $name = 'DomainModel';
static get fields() {
return [
{ name: 'domainId', dataSource: 'domainId' },
{ name: 'name', dataSource: 'label' },
];
}
static get idField() {
return 'domainId';
}
}
const domainStore = new Store({
id: 'domains',
modelClass: DomainModel
});
class DomainColumn extends Column {
static $name = 'DomainColumn';
static type = 'domainColumn';
static get isGanttColumn() {
return true;
}
static get defaults() {
return {
text: 'Domain',
field: 'domainId',
renderer({ value }) {
const domain = domainStore.getById(value);
return domain ? domain.name : '';
},
editor: {
displayField: 'name',
valueField: 'domainId',
type: 'combo',
store: domainStore,
},
};
}
}
// Register the column type for use below
ColumnStore.registerColumnType(DomainColumn);
const project = new ProjectModel({
loadUrl : '../_datasets/launch-saas.json',
syncUrl : '/pretendSyncUrl',
autoLoad : true,
autoSync : true,
autoSetConstraints : true,
taskModelClass: CustomTaskModel,
taskStore: {
// Set default transform flat tasks to task tree
transformFlatData: true,
// Enable WBS column to update every time a task order changes
wbsMode: 'auto',
},
listeners: {
beforeSend(event) {
console.log(`-> beforeSend:`);
console.log(event);
},
beforeLoad(event) {
console.log(`--> beforeLoad`);
// Adding the "domains" store to the request
event.pack.stores = ['calendars', 'dependencies', 'tasks', 'domains'];
},
beforeLoadApply(event) {
console.log(`--> beforeLoadApply`);
// Simulate the domain data in the response
event.response.domains = {
rows: [
{ domainId: '201', label: 'Software' },
{ domainId: '202', label: 'Hardware' }
]
};
// Override the default task data
event.response.tasks.rows = _tasks;
},
beforeSync({ pack }) {
console.log(`--> beforeSync`);
console.log(pack);
},
}
});
// Add the custom store to the project
project.addCrudStore(domainStore);
new Gantt({
appendTo : 'container',
rowHeight : 45,
tickSize : 45,
barMargin : 8,
project : project,
scrollTaskIntoViewOnCellClick : true,
selectionMode : {
cell : false,
dragSelect : true,
},
rowReorder: {
showGrip: true,
},
columns : [
{ type : 'wbs' },
{ type : 'name', width : 250 },
{ type : 'domainColumn', name: 'Domain', width : 250 }
],
features: {
taskEdit: {
items: {
generalTab: {
items: {
percentDone: false,
effort: false,
domain: {
type: 'combo',
flex: '1 1 100%',
label: 'Domain',
displayField: 'name',
valueField: 'domainId',
store: domainStore,
},
}
}
},
},
},
listeners: {
beforeTaskEditShow({ editor, taskRecord }) {
editor.widgetMap.domain.value = domainStore.getById(taskRecord.domainId);
},
beforeTaskSave(event) {
console.log(`${event.taskRecord.name} - ${event.taskRecord.domainId}`);
}
},
});