Our state of the art Gantt chart


Post by Jerther »

Hi!

Here's some basic code that adds a checkbox for the rollup field in the task editor popup:
import {Gantt, ProjectModel} from './gantt.module.js';

const project = new ProjectModel({
    transport : {
        load : {
            url : 'data2.json'
        }
    }
});

new Gantt({
    adopt : document.body,

    project : project,

    columns : [
        { type : 'name', field : 'name', width : 250 }
    ],

    features: {
        taskEdit: {
            editorConfig: {
                extraItems: {
                    generaltab: [
                        {
                            type: 'checkbox',
                            label: "rollup test",
                            name: 'rollup',
                            flex: '1 0 50%',
                        }
                    ]
                }
            }
        }
    }
});

project.load();
If your toggle the newly added checkbox, it also toggls it in the advanced tab. It works fine when the new field is in generaltab and advancedtab (yes having the same field twice in the same tab works fine).

Now, change generaltab to resourcestab and try again. The checkbox in the advanced tab does not follow! And if you leave the new field ON, save, and come back, the value is not restored. If you check the original checkbox, then the new one toggles as well. The value is properly loaded too in the new field. It's just not saved.

In fact, the actual problem I'm having is that I added a new field and the value is not saved to the store if the editor is in the resourcetab but it works fine if I place it in advancedtab or generaltab. I believe the fundamental issue is the same.

Not sure if it's a bug or if I'm missing something... :)

Using Gantt v2.1.0
Last edited by Jerther on Wed Apr 15, 2020 5:37 pm, edited 1 time in total.

Post by mats »

This is by design. General + Advanced tabs are forms, whereas the other tabs are grid containers where we do not expect form fields to be placed. Do you actually want to have form fields with state in the grid container tabs?

Post by Jerther »

Yes the full use case is to have a Combo and a Button in the resource tab and depending on the selection in the Combo, when clicking the Button, different resources are added/removed to/from the resource grid so to automate the addition of around 20 resources one by one. I could place the widgets elsewhere but we'll agree the resourcetab is the most intuitive place.

Post by Jerther »

I'm starting to understand I would have to extend ResourcesTab (not a lot of doc there, is it...), somehow add my new components to it (Combo and Button), somehow have them interract with the grid store, and somehow plug this into the task editor, probably replacing the original tab.

Anyway, that's a lot of guesswork, not much documentation, any help would be very welcome.

Post by pmiklashevich »

Hello,

It's really easy to customize tabs in Task Editor. If having tabsConfig and extraItems configs is not enough, you can always use a hook to add your fields, our customize ours. Please check out beforeTaskEditShow event. You can setup your fields there. Please copy this example code to the Gantt/examples/taskeditor/app.js and check how it works:
const gantt = new Gantt({
    listeners : {
        beforeTaskEditShow({ taskEdit, editor }) {
            if (!editor.initializedCustomFields) {
                const
                    container       = editor.widgetMap.resourcestab.items[0],
                    [combo, button] = container.add([
                        {
                            type  : 'combo',
                            label : 'Select options',
                            flex  : 1,
                            style : 'margin : 0 0.5em 0 0',
                            items : [
                                { value : 3, text : 'First 3 resources' },
                                { value : 5, text : 'First 5 resources' }
                            ]
                        },
                        {
                            type    : 'button',
                            text    : 'Apply',
                            onClick : async () => {
                                if (editor.myResourceSelector.value) {
                                    const
                                        project = taskEdit.getProject(),
                                        resources = project.resourceStore.getRange().slice(0, editor.myResourceSelector.value);

                                    // project.taskStore.removeAssignmentsForEvent(taskEdit.record);
                                    const oldResources = taskEdit.record.assignments.map(assignment => assignment.resource);

                                    for (const resource of oldResources) {
                                        await taskEdit.record.unassign(resource);
                                    }

                                    // TODO: https://github.com/bryntum/support/issues/540
                                    // project.taskStore.assignEventToResource(taskEdit.record, resources, true);
                                    for (const resource of resources) {
                                        await taskEdit.record.assign(resource);
                                    }
                                }
                            }
                        }
                    ]);

                editor.myResourceSelector = combo;

                editor.initializedCustomFields = true;
            }

            editor.myResourceSelector.value = 3;
        }
    },

    adopt : 'container',

    taskRenderer : ({ taskRecord, tplData }) => {
        if (taskRecord.color) {
            tplData.style += `background-color:${taskRecord.color}`;
        }
    },

    columns : [
        { type : 'name', field : 'name', text : 'Name', width : 250 },
        { type : 'date', field : 'deadline', text : 'Deadline' }
    ],

    project
});
Basically assignEventToResource should work, but unfortunately it's not. I've created a ticket to get it fixed: https://github.com/bryntum/support/issues/540
Meanwhile you can use a workaround. Each call of assign/unassign triggers propagation and returns a Promise. The workaround uses public API, so the code is valid, but a bit slow. You might experience this if you have lots of resources. When the ticket is fixed you can replace the code with the commented out line.
Снимок экрана 2020-04-13 в 22.06.45.png
Снимок экрана 2020-04-13 в 22.06.45.png (201.42 KiB) Viewed 2143 times
Best,
Pavel

Pavlo Miklashevych
Sr. Frontend Developer


Post by Jerther »

Neat!

But it's back to the problem in the original post. If I assign a record field to the Combo:
{
    type  : 'combo',
    label : 'Select options',
    flex  : 1,
    style : 'margin : 0 0.5em 0 0',
    name: 'resourceGroup',  // <-- HERE
    store   : self.model.resourceGroupsStore,
    displayField: 'name',
    valueField: 'id',
},
The value loads fine, but it is not saved when I click the save button. Even if I force a sync by also changing another field, only the other field is saved.

I also tried with "items" instead of a "store" and I get the same result. :cry:

Post by pmiklashevich »

If you want to use 'name' and map value, instead of loading value in beforeTaskEditShow function and saving data in beforeTaskSave function, you need to define your model, and define your 'resourceGroup' field:
class MyEventModel extends EventModel {
    static get fields() {
        return [
            { name : 'resourceGroup' }
        ];
    }
}
Then set modelClass for your store.

Pavlo Miklashevych
Sr. Frontend Developer


Post by Jerther »

I think you meant TaskModel ;)

Still, extending TaskModel does not change anything. I think the autoExposeFields property of ProjectModel is true by default anyway.

Here's a simplified version to illustrate what's happening. The button works fine in your example so I omitted it here for clarity:
import {Gantt, ProjectModel, TaskModel} from './gantt.module.js';

class MyEventModel extends TaskModel {
    static get fields() {
        return [
            { name : 'resourceGroup' }
        ];
    }
}


const project = new ProjectModel({
    autoSync: true,
    modelClass: MyEventModel,  // Setting this or not makes no difference
    transport : {
        load : {
            url : 'data2.json'
        },
        sync : {
            url : 'sync',
        }
    }
});

new Gantt({
    adopt : document.body,

    project : project,

    columns : [
        { type : 'name', field : 'name', width : 250 }
    ],

    taskRenderer({taskRecord, renderData}) {
        if (taskRecord.isLeaf && !taskRecord.isMilestone)
            return taskRecord.name;
    },

    listeners : {
        beforeTaskEditShow({ taskEdit, editor }) {
            if (!editor.initializedCustomFields) {
                const
                    container       = editor.widgetMap.resourcestab.items[0],
                    [combo, button] = container.add([
                        {
                            type  : 'combo',
                            label : 'Select options',
                            flex  : 1,
                            style : 'margin : 0 0.5em 0 0',
                            name : 'resourceGroup',  // <-- I added this so the value is loaded from the record, and also saved to it
                            items : [
                                { value : 3, text : 'First 3 resources' },
                                { value : 5, text : 'First 5 resources' }
                            ]
                        }
                    ]);

                editor.myResourceSelector = combo;

                editor.initializedCustomFields = true;
            }
        }
    },
});

project.load();
And also data2.json:
{
	"success": true,
	"project": {
		"startDate": "2020-03-31 04:00:00"
	},
	"tasks": {
		"rows": [{
			"id": 38,
			"name": "New task 1",
			"startDate": "2020-03-31 04:00:00",
			"endDate": "2020-04-13 04:00:00",
			"constraintType": "startnoearlierthan",
			"constraintDate": "2020-03-31 04:00:00",
			"parentIndex": 3,
			"resourceGroup": 5
		},
		{
			"id": 39,
			"name": "New task 2",
			"startDate": "2020-04-10 04:00:00",
			"endDate": "2020-04-12 04:00:00",
			"constraintType": "startnoearlierthan",
			"constraintDate": "2020-04-10 04:00:00",
			"parentIndex": 4,
			"resourceGroup": null
		}]
	},
	"resources": {
        "rows": [
            {"id": 1, "name": "Doris"},
            {"id": 2, "name": "Stephen"}
        ]
	}
}
Now if you open the first task, the resourceModel field is set to "First 5 resources" and this reflects what was loaded from the json. The other task has nothing selected which is also fine. Now watch the network activity and in this task, set the resourceGroup to "First 3 resources" and hit save: nothing happens. And if you open this task again, the resourceGroup value is gone.

And to clarify, if the Combo is inserted in the General tab:
features: {
        taskEdit: {
            editorConfig: {
                extraItems: {
                    generaltab: [
                        {
                            type  : 'combo',
                            label : 'Select options',
                            flex  : 1,
                            style : 'margin : 0 0.5em 0 0',
                            name : 'resourceGroup',
                            items : [
                                { value : 3, text : 'First 3 resources' },
                                { value : 5, text : 'First 5 resources' }
                            ]
                        }
                    ]
                }
            }
        }
    }
When you hit that save button, a sync call is made with this data:
{
	"type": "sync",
	"requestId": 15868775081682,
	"tasks": {
		"updated": [{
			"resourceGroup": 3,
			"id": 39
		}]
	}
}
Which is what I expect.

Now mats already said:
mats wrote:This is by design. General + Advanced tabs are forms, whereas the other tabs are grid containers where we do not expect form fields to be placed.
So I believe what I'm looking for is a workaround...

Post by pmiklashevich »

Hello,

I wouldn't rely on auto exposing. If the first record doesn't have this field, it will be missing in the model field definition. So I would recommend to describe your field explicitly.
modelClass: MyEventModel,  // Setting this or not makes no difference
That's because config is called https://www.bryntum.com/docs/gantt/#Gantt/model/ProjectModel#config-taskModelClass
Jerther wrote: Tue Apr 14, 2020 5:26 pm Now mats already said:
mats wrote:This is by design. General + Advanced tabs are forms, whereas the other tabs are grid containers where we do not expect form fields to be placed.
So I believe what I'm looking for is a workaround...
Yes, sure. Basically Gantt/widget/taskeditor/FormTab adds 'change' listener for all widgets in the tab and in the listener if the value in the widget is valid it sets value to the record and requests propagation. You can easily implement it yourself. Let me give you an example.

For that please edit Gantt/examples/_datasets/launch-saas.json:
                            {
                                "id": 11,
                                "name": "Install Apache",
                                "resourceGroup": 5,
Edit Gantt/examples/basic/app.js:
import TaskModel from '../../lib/Gantt/model/TaskModel.js';

class MyEventModel extends TaskModel {
    static get fields() {
        return [
            { name : 'resourceGroup' }
        ];
    }
}

const project = new ProjectModel({
    taskModelClass : MyEventModel,
    transport      : {
        load : {
            url : '../_datasets/launch-saas.json'
        }
    }
});

const gantt = new Gantt({
    listeners : {
        beforeTaskEditShow({ taskEdit, editor }) {
            if (!editor.initializedCustomFields) {
                const tab = editor.widgetMap.resourcestab;

                tab.items[0].add([
                    {
                        type  : 'combo',
                        label : 'Select options',
                        flex  : 1,
                        style : 'margin : 0',
                        name  : 'resourceGroup',
                        ref   : 'my-combo',
                        items : [
                            { value : 3, text : 'First 3 resources' },
                            { value : 5, text : 'First 5 resources' }
                        ],
                        onChange({ source : combo, value }) {
                            const project = tab.getProject();

                            if (!tab._loading && combo.isValid && project && !project.isPropagating()) {
                                tab.record.resourceGroup = value;
                                tab.requestPropagation();
                            }
                        }
                    }
                ]);

                editor.initializedCustomFields = true;
            }
        }
    },
Now please open the Basic demo locally and run in console:
gantt.taskStore.getById(11).$name
// "MyEventModelEx"
gantt.taskStore.getById(11).resourceGroup
// 5
Now edit "Install Apache".
Go to Resources tab.
See "5" is selected in the combo.

Now change it to 3 and click Save.
Run in console:
gantt.taskStore.getById(11).resourceGroup
// 3
gantt.taskStore.getById(11).modifications
// {resourceGroup: 3, id: 11}
Hope this will help you!

Cheers,
Pavel

Pavlo Miklashevych
Sr. Frontend Developer


Post by Jerther »

pmiklashevich wrote:That's because config is called Gantt.model.ProjectModel#taskModelClass
:shock: Oh no..... :oops:
pmiklashevich wrote:If the first record doesn't have this field
My backend always provide all fields so I don't fear a problem at that level. The JS side receives more fields than it needs and I think it may be suboptimal as the dataset grows but for now it's fine.
pmiklashevich wrote:Hope this will help you!
It sure does! Everything works now, thanks to your precious help! Thank you very much for the detailed explanations, Pavel. It will certainly be useful again later :D

Post Reply