Premium support for our pure JavaScript UI components


Post by msaliba »

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:

  1. 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.

  2. 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.

  3. 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}`); } }, });

Post by mats »

One thought, if you skip the dataSource mapping for parentId, and instead add:

static get parentIdField() {
        return 'containerId';
    }

Does that make it work (also add a field called containerId btw)?


Post by msaliba »

Thanks Mats, that worked and solves the first issue. Out of curiosity, where in the docs could I have found "parentIdField()"?

For the second, I tried "parentIndexField()" to map the "sortOrder" field but that had no effect (in the example I shared, I had to change "containerId" to "parentId" and "sortOrder" to "parentIndex" to make it work). As "parentIndex", the child tasks get sorted correctly, but not the two tasks at the root level.


Post by mats »

Out of curiosity, where in the docs could I have found "parentIdField()"?

Nowhere, private property - we'll discuss if it could be made public.

Will check 2nd issue shortly.


Post by mats »

2nd issue looks like a bug, should be fixed for next patch release. https://github.com/bryntum/support/issues/10953

First issue, ticket here: https://github.com/bryntum/support/issues/10206


Post by msaliba »

Thanks Mats, we'll keep an eye on the bug and the future release notes.


Post Reply