Our state of the art Gantt chart


Post by vconstruct »

Hi,

In Bryntum Gantt TaskEdit popup, we have a custom tab in which we are using the Combo dropdown widget. We are able to load the data and achieve the desired functionality except for the following issues, for which we could use some help.

  1. ' text field ' is not cleared/reset when the task editor is closed : When the task editor is closed, the 'text field' in the Combo (dropdown) widget does not reset, so when we try to reopen the task editor the previously selected multiple options still show up in the text field. (please see updated code for Bryntum 'Advanced' example.

  2. We have added a 'Delete' button in the Grid (seen in the Custom Tab config code below) and 'Add' button in the toolbar and we'd like to refresh the grid data instantly on button click without having to close the editor. Please advise on how this can be achieved.

Updated code for Bryntum Gantt Advanced example (with combo widget)

import { Combo, StringHelper, Grid, List, Gantt, ProjectModel, TaskModel } from '../../build/gantt.module.js?467719';
import shared from '../_shared/shared.module.js?467719';

const baseColors = [
    'maroon', 'red', 'orange', 'yellow',
    'olive', 'green', 'purple', 'fuchsia',
    'lime', 'teal', 'aqua', 'blue', 'navy',
    'black', 'gray', 'silver', 'white'
];

class ColorField extends Combo {
    // Factoryable type name
    static get type() {
        return 'colorfield';
    }

static get defaultConfig() {
    return {
        clearable : true,
        items     : baseColors,
        picker    : {
            cls     : 'b-color-picker-container',
            itemCls : 'b-color-picker-item',
            itemTpl : item => `<div style="background-color:${item.id}"></div>`
        }
    };
}
}

// Register this widget type with its Factory
ColorField.initClass();

/**
 * @module FilesTab
 */

/**
 * @internal
 */
class FilesTab extends Grid {

    // Factoryable type name
    static get type() {
        return 'filestab';
    }

    static get defaultConfig() {
        return {
            title    : 'Files',
            defaults : {
                labelWidth : 200
            },
            columns : [{
                text     : 'Files attached to task',
                field    : 'name',
                type     : 'template',
                template : data => StringHelper.xss`<i class="b-fa b-fa-fw b-fa-${data.record.data.icon}"></i>${data.record.data.name}`
            }]
        };
    }

    loadEvent(eventRecord) {
        const files = [];

for (let i = 0; i < Math.random() * 10; i++) {
    const nbr = Math.round(Math.random() * 5);

    switch (nbr) {
        case 1:
            files.push({
                name : `Image${nbr}.pdf`,
                icon : 'image'
            });
            break;
        case 2:
            files.push({
                name : `Charts${nbr}.pdf`,
                icon : 'chart-pie'
            });
            break;
        case 3:
            files.push({
                name : `Spreadsheet${nbr}.pdf`,
                icon : 'file-excel'
            });
            break;
        case 4:
            files.push({
                name : `Document${nbr}.pdf`,
                icon : 'file-word'
            });
            break;
        case 5:
            files.push({
                name : `Report${nbr}.pdf`,
                icon : 'chart-line'
            });
            break;
    }
}

this.store.data = files;
    }
}

// Register this widget type with its Factory
FilesTab.initClass();

/**
 * @module ResourceList
 */

/**
 * @internal
 */
class ResourceList extends List {

    // Factoryable type name
    static get type() {
        return 'resourcelist';
    }

    static get configurable() {
        return {
            title   : 'Resources',
            cls     : 'b-inline-list',
            items   : [],
            itemTpl : resource => {
                return StringHelper.xss`
                    <img src="../_shared/images/users/${resource.name.toLowerCase()}.jpg">
                    <div class="b-resource-detail">
                        <div class="b-resource-name">${resource.name}</div>
                        <div class="b-resource-city">
                            ${resource.city}
                            <i data-btip="Deassign resource" class="b-icon b-icon-trash"></i>
                        </div>
                    </div>
                `;
            }
        };
    }

    // Called by the owning TaskEditor whenever a task is loaded
    loadEvent(taskRecord) {
        this.taskRecord = taskRecord;
        this.store.data = taskRecord.resources;
    }

    // Called on item click
    onItem({ event, record }) {
        if (event.target.matches('.b-icon-trash')) {
            // Unassign the clicked resource record from the currehtly loaded task
            this.taskRecord.unassign(record);

    // Update our store with the new assignment set
    this.store.data = this.taskRecord.resources;
}
    }
}

// Register this widget type with its Factory
ResourceList.initClass();

class MyModel extends TaskModel {
    static get fields() {
        return [
            { name : 'deadline', type : 'date' },
            { name : 'color' }
        ];
    }
}

const project = window.project = new ProjectModel({
    taskModelClass : MyModel,
    transport      : {
        load : {
            url : '../_datasets/launch-saas.json'
        }
    },
    // This config enables response validation and dumping of found errors to the browser console.
    // It's meant to be used as a development stage helper only so please set it to false for production systems.
    validateResponse : true
});

const gantt = new Gantt({
    appendTo : 'container',

features : {
    taskEdit : {
        items : {
            generalTab : {
                // change title of General tab
                title : 'Common',
                items : {
                    customDivider : {
                        html    : '',
                        dataset : {
                            text : 'Custom fields'
                        },
                        cls  : 'b-divider',
                        flex : '1 0 100%'
                    },
                    deadlineField : {
                        type  : 'datefield',
                        name  : 'deadline',
                        label : 'Deadline',
                        flex  : '1 0 50%',
                        cls   : 'b-inline'
                    },
                    colorField : {
                        type  : 'colorfield',
                        name  : 'color',
                        label : 'Color',
                        flex  : '1 0 50%',
                        cls   : 'b-inline'
                    },
                    priority : {
                        type    : 'radiogroup',
                        name    : 'priority',
                        label   : 'Priority',
                        flex    : '1 0 100%',
                        options : {
                            high : 'High',
                            med  : 'Medium',
                            low  : 'Low'
                        }
                    },
      myCombo: {
        type: 'combo',
        label: 'Select an HRA',
        items: ['A','B','C','D'],
        editable: true,
        multiSelect: true,
        style: {
          padding: '5px',
        },
        clearTextOnPickerHide: true
      },

                }
            },
            // remove Notes tab
            notesTab : false,
            // add custom Files tab to the second position
            filesTab : {
                type   : 'filestab',
                weight : 110
            },
            // add custom Resources tab to the third position
            resourcesTab : {
                type   : 'resourcelist',
                weight : 120,
                title  : 'Resources'
            },
            // customize the predecessors grid
            predecessorsTab : {
                items : {
                    grid : {
                        columns : {
                            data : {
                                // Our definition of the name column
                                // is merged into the existing one.
                                // We just add some configurations.
                                name : {
                                    // Grid cell values are rendered with extra info.
                                    renderer({ record : dependency }) {
                                        const predecessorTask = dependency.fromTask;
                                        if (predecessorTask) {
                                            return StringHelper.xss`${predecessorTask.name} (${predecessorTask.id})`;
                                        }
                                        return '';
                                    },
                                    // The cell editor, and its dropdown list
                                    // also have this extra info.
                                    editor : {
                                        displayValueRenderer(taskRecord) {
                                            return taskRecord ? StringHelper.xss`${taskRecord.name} (${taskRecord.id})` : '';
                                        },
                                        listItemTpl(taskRecord) {
                                            return StringHelper.xss`${taskRecord.name} (${taskRecord.id})`;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
},

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

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

project,

dependencyIdField : 'sequenceNumber'
});

project.load();

Custom Tab config for reference

getHRATabTaskEditConfig = (
  HRAcomboItems = ['HRA1', 'HRA2', 'HRA3'],
  emitHraInputForAssign: (arg0: any) => void,
  addHRAsTrigger: () => void,
  removeHRATrigger: () => void,
  emitHRAInputForRemove: (arg0: any) => void,
) => {


  const onHraInput = (event: any) => {
    emitHraInputForAssign(event.source._valueCollection?._values);

  };

  const addHRAs = ({ source : { parent : { parent : { parent : taskEditor } } } }: { source: { parent: { parent: { parent: any } } } }) => {
    addHRAsTrigger();
    // taskEditor.close() // I get an error message saying taskEditor.close() is not defined. Maybe the button from toolbar triggers a different event?
  }

  const removeHRAs = (event: Event | undefined, btn: any) => {
    emitHRAInputForRemove(btn);
    removeHRATrigger()
  }

  return {
    HRATab: {
      scrollIntoView: true,
      title: 'HRA',
      weight: 500,
      items: {
        grid: {
          width: '100%',
          height: '60%',
          style: { overflowX: 'scroll' },
          type: 'grid',
          columns: {
            data: {
              hraid: {
                field: 'hra_id',
                text: 'HRA Id',
                width: '45%',
                editor: {
                  readOnly: true,
                },
                style: {
                  borderLeft: '1px solid #000',
                },
              },
              hradescription: {
                field: 'hra_description',
                text: 'HRA Description',
                width: '45%',
                editor: {
                  readOnly: true,
                },
              },
              delete: {
                type: 'widget',
                field: 'widget',
                width: '10%',
                widgets: [
                  {
                    type: 'button',
                    icon: `<i class="b-fa b-fa-trash b-black"></i>`,
                    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                    // @ts-ignore
                    onClick: ({ event, source: btn }) => {
                      removeHRAs(event, btn)
                    }
                  },
                ],
              },
            },
          },
        },
        myCombo: {
          type: 'combo',
          label: 'Select an HRA',
          listeners: {
            change: onHraInput,
          },
          value:'',
          items: HRAcomboItems,
          editable: true,
          multiSelect: true,
          style: {
            padding: '5px',
          },
          clearTextOnPickerHide: true
        },
        toolbar: {
          type: 'toolbar',
          width: '50%',
          items: {
            addButton: { text: 'Add', 
            icon: 'b-fa b-fa-plus', 
            onClick: addHRAs,
          },
          cancelButton: { 
            text: 'Cancel', 
            icon: 'b-fa b-fa-times' ,
            // onClick: resetMultiSelect 
          },
          },
        },
      },
    },
  };
};

Post by alex.l »

Hi,

' text field ' is not cleared/reset when the task editor is closed

You can subscribe on https://bryntum.com/products/gantt/docs/api/SchedulerPro/feature/TaskEdit#event-beforeTaskEditShow and set all values you need in fields for state you want.

we'd like to refresh the grid data instantly on button click without having to close the editor.

Not sure I follow this one. When you change data in grid's store, it will be always automatically visible in the grid. If you operate with something else and not grid store, set data into grid store on button click.
https://bryntum.com/products/gantt/docs/api/Core/data/Store#property-data

grid.store.data = newData;

All the best,
Alex


Post by fabio.mazza »

Hello,
regarding first point, in addition on what Alex has just mentioned, try to add name property in your "myCombo" widget. I've just tried in my local environment and it worked after this change.

                     myCombo : {
                        type: 'combo',
                        name    : 'myCombo', // missing
                        label: 'Select an HRA',
                        items: ['A','B','C','D'],
                        editable: true,
                        multiSelect: true,
                        style: {
                            padding: '5px',
                        },
                        clearTextOnPickerHide: true
                    },

Best regards,
Fabio


Post by vconstruct »

Hi Alex and Fabio,

Thanks for the response. I added the 'name' property to the 'myCombo' config and I am able to access the combo in beforeTaskEditShow, but I couldn't find the method that I can use to reset the textField in the combo. Can you please advise us on available methods to reset the textField? Is it required to use the 'value' property, in the custom Tab config, to be able to reset it?

I have provided the code below for reference.

listeners={{
          beforeTaskEditShow(event: any) {
            const { editor, taskRecord } = event;
            const widgetMap = editor.widgetMap;
            const myCombo = widgetMap.tradePartnersTab.widgetMap.myCombo
          },
        }}

Post by tasnim »

Hi,

You could set this https://bryntum.com/products/gantt/docs/api/Core/widget/Combo#property-value to null or empty string to reset the combo

combo.value = null;

And if you want to reset the text field then, you could set https://bryntum.com/products/gantt/docs/api/Core/widget/TextField#property-value to an empty string

textField.value = '';

Hope this will help.


Post by vconstruct »

Hi Tasnim,

Thank you for the response. Setting the combo.value to null worked.


Post Reply