Premium support for our pure JavaScript UI components


Post by jit@flowit.dk »

Hi,

Today I enabled multi selection in Gantt. This required us to also enable the showGrip feature of the rowReorderFeature, along with gripOnly - but now I'm unable to move anything - the grip handle doesn't work.

After some investigation I figured the problem was related to the checkbox column type which happens to be the very first one in my configuration:

Screenshot 2023-03-30 at 12.03.11.png
Screenshot 2023-03-30 at 12.03.11.png (387.8 KiB) Viewed 145 times

The problem can be reproduced using the online example:
https://bryntum.com/products/gantt/examples/advanced/

  • Disable selectionMode.rowNumber
  • Add a checkbox column as the first one
  • enable features.rowReorder.gripOnly
Bryntum-2023-03-30-008.png
Bryntum-2023-03-30-008.png (371.33 KiB) Viewed 145 times
Screenshot 2023-03-30 at 12.05.23.png
Screenshot 2023-03-30 at 12.05.23.png (159.22 KiB) Viewed 145 times

Now you won't be able to move anything around.

Full code sample for the online demo:

import { Toolbar, Toast, DateHelper, CSSHelper, Column, ColumnStore, TaskModel, Gantt } from '../../build/gantt.module.js?466404';
import shared from '../_shared/shared.module.js?466404';

/**
 * @module GanttToolbar
 */

/**
 * @extends Core/widget/Toolbar
 */
class GanttToolbar extends Toolbar {
    // Factoryable type name
    static get type() {
        return 'gantttoolbar';
    }

    static get $name() {
        return 'GanttToolbar';
    }

    static get configurable() {
        return {
            items : [
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            color    : 'b-green',
                            ref      : 'addTaskButton',
                            icon     : 'b-fa b-fa-plus',
                            text     : 'Create',
                            tooltip  : 'Create new task',
                            onAction : 'up.onAddTaskClick'
                        }
                    ]
                },
                {
                    ref   : 'undoRedo',
                    type  : 'undoredo',
                    items : {
                        transactionsCombo : null
                    }
                },
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            ref      : 'expandAllButton',
                            icon     : 'b-fa b-fa-angle-double-down',
                            tooltip  : 'Expand all',
                            onAction : 'up.onExpandAllClick'
                        },
                        {
                            ref      : 'collapseAllButton',
                            icon     : 'b-fa b-fa-angle-double-up',
                            tooltip  : 'Collapse all',
                            onAction : 'up.onCollapseAllClick'
                        }
                    ]
                },
                {
                    type  : 'buttonGroup',
                    items : [
                        {
                            ref      : 'zoomInButton',
                            icon     : 'b-fa b-fa-search-plus',
                            tooltip  : 'Zoom in',
                            onAction : 'up.onZoomInClick'
                        },
                        {
                            ref      : 'zoomOutButton',
                            icon     : 'b-fa b-fa-search-minus',
                            tooltip  : 'Zoom out',
                            onAction : 'up.onZoomOutClick'
                        },
                        {
                            ref      : 'zoomToFitButton',
                            icon     : 'b-fa b-fa-compress-arrows-alt',
                            tooltip  : 'Zoom to fit',
                            onAction : 'up.onZoomToFitClick'
                        },
                        {
                            ref      : 'previousButton',
                            icon     : 'b-fa b-fa-angle-left',
                            tooltip  : 'Previous time span',
                            onAction : 'up.onShiftPreviousClick'
                        },
                        {
                            ref      : 'nextButton',
                            icon     : 'b-fa b-fa-angle-right',
                            tooltip  : 'Next time span',
                            onAction : 'up.onShiftNextClick'
                        }
                    ]
                },
                {
                    type      : 'datefield',
                    ref       : 'startDateField',
                    label     : 'Project start',
                    // required  : true, (done on load)
                    flex      : '0 0 18em',
                    listeners : {
                        change : 'up.onStartDateChange'
                    }
                },
                {
                    type         : 'combo',
                    ref          : 'projectSelector',
                    label        : 'Choose project',
                    editable     : false,
                    width        : '25em',
                    displayField : 'name',
                    value        : 1,
                    store        : {
                        data : [
                            {
                                id   : 1,
                                name : 'Launch SaaS',
                                url  : '../_datasets/launch-saas.json'
                            },
                            {
                                id   : 2,
                                name : 'Build web app for customer',
                                url  : '../_datasets/tasks-workedhours.json'
                            }
                        ]
                    },
                    listeners : {
                        select : 'up.onProjectSelected'
                    }
                },
                '->',
                {
                    type                 : 'textfield',
                    ref                  : 'filterByName',
                    cls                  : 'filter-by-name',
                    flex                 : '0 0 13.5em',
                    // Label used for material, hidden in other themes
                    label                : 'Find tasks by name',
                    // Placeholder for others
                    placeholder          : 'Find tasks by name',
                    clearable            : true,
                    keyStrokeChangeDelay : 100,
                    triggers             : {
                        filter : {
                            align : 'end',
                            cls   : 'b-fa b-fa-filter'
                        }
                    },
                    onChange : 'up.onFilterChange'
                },
                {
                    type    : 'button',
                    ref     : 'featuresButton',
                    icon    : 'b-fa b-fa-tasks',
                    text    : 'Settings',
                    tooltip : 'Toggle features',
                    menu    : {
                        onItem       : 'up.onFeaturesClick',
                        onBeforeShow : 'up.onFeaturesShow',
                        // "checked" is set to a boolean value to display a checkbox for menu items. No matter if it is true or false.
                        // The real value is set dynamically depending on the "disabled" config of the feature it is bound to.
                        items        : [
                            {
                                text : 'UI settings',
                                icon : 'b-fa-sliders-h',
                                menu : {
                                    cls         : 'settings-menu',
                                    layoutStyle : {
                                        flexDirection : 'column'
                                    },
                                    onBeforeShow : 'up.onSettingsShow',
                                    defaults     : {
                                        type      : 'slider',
                                        showValue : true
                                    },
                                    items : [
                                        {
                                            ref     : 'rowHeight',
                                            text    : 'Row height',
                                            min     : 30,
                                            max     : 70,
                                            onInput : 'up.onRowHeightChange'
                                        },
                                        {
                                            ref     : 'barMargin',
                                            text    : 'Bar margin',
                                            min     : 0,
                                            max     : 10,
                                            onInput : 'up.onBarMarginChange'
                                        },
                                        {
                                            ref     : 'duration',
                                            text    : 'Animation duration',
                                            min     : 0,
                                            max     : 2000,
                                            step    : 100,
                                            onInput : 'up.onAnimationDurationChange'
                                        },
                                        {
                                            ref     : 'radius',
                                            text    : 'Dependency radius',
                                            min     : 0,
                                            max     : 10,
                                            onInput : 'up.onDependencyRadiusChange'
                                        }
                                    ]
                                }
                            },
                            {
                                text    : 'Draw dependencies',
                                feature : 'dependencies',
                                checked : false
                            },
                            {
                                text    : 'Task labels',
                                feature : 'labels',
                                checked : false
                            },
                            {
                                text    : 'Critical paths',
                                feature : 'criticalPaths',
                                tooltip : 'Highlight critical paths',
                                checked : false
                            },
                            {
                                text    : 'Project lines',
                                feature : 'projectLines',
                                checked : false
                            },
                            {
                                text    : 'Highlight non-working time',
                                feature : 'nonWorkingTime',
                                checked : false
                            },
                            {
                                text    : 'Enable cell editing',
                                feature : 'cellEdit',
                                checked : false
                            },
                            {
                                text    : 'Show column lines',
                                feature : 'columnLines',
                                checked : true
                            },
                            {
                                text    : 'Show baselines',
                                feature : 'baselines',
                                checked : false
                            },
                            {
                                text    : 'Show rollups',
                                feature : 'rollups',
                                checked : false
                            },
                            {
                                text    : 'Show progress line',
                                feature : 'progressLine',
                                checked : false
                            },
                            {
                                text    : 'Show parent area',
                                feature : 'parentArea',
                                checked : false
                            },
                            {
                                text         : 'Stretch tasks to fill ticks',
                                toggleConfig : 'fillTicks',
                                checked      : false
                            },
                            {
                                text    : 'Hide schedule',
                                cls     : 'b-separator',
                                subGrid : 'normal',
                                checked : false
                            }
                        ]
                    }
                }
            ]
        };
    }

    // Called when toolbar is added to the Gantt panel
    updateParent(parent, was) {
        super.updateParent(parent, was);

this.gantt = parent;

parent.project.on({
    load    : 'updateStartDateField',
    refresh : 'updateStartDateField',
    thisObj : this
});

this.styleNode = document.createElement('style');
document.head.appendChild(this.styleNode);
    }

setAnimationDuration(value) {
    const
        me      = this,
        cssText = `.b-animating .b-gantt-task-wrap { transition-duration: ${value / 1000}s !important; }`;

    me.gantt.transitionDuration = value;

    if (me.transitionRule) {
        me.transitionRule.cssText = cssText;
    }
    else {
        me.transitionRule = CSSHelper.insertRule(cssText);
    }
}

updateStartDateField() {
    const { startDateField } = this.widgetMap;

    startDateField.value = this.gantt.project.startDate;

    // This handler is called on project.load/propagationComplete, so now we have the
    // initial start date. Prior to this time, the empty (default) value would be
    // flagged as invalid.
    startDateField.required = true;
}

// region controller methods

async onAddTaskClick() {
    const
        { gantt } = this,
        added     = gantt.taskStore.rootNode.appendChild({ name : this.L('New task'), duration : 1 });

    // run propagation to calculate new task fields
    await gantt.project.commitAsync();

    // scroll to the added task
    await gantt.scrollRowIntoView(added);

    gantt.features.cellEdit.startEditing({
        record : added,
        field  : 'name'
    });
}

onEditTaskClick() {
    const { gantt } = this;

    if (gantt.selectedRecord) {
        gantt.editTask(gantt.selectedRecord);
    }
    else {
        Toast.show(this.L('First select the task you want to edit'));
    }
}

onExpandAllClick() {
    this.gantt.expandAll();
}

onCollapseAllClick() {
    this.gantt.collapseAll();
}

onZoomInClick() {
    this.gantt.zoomIn();
}

onZoomOutClick() {
    this.gantt.zoomOut();
}

onZoomToFitClick() {
    this.gantt.zoomToFit({
        leftMargin  : 50,
        rightMargin : 50
    });
}

onShiftPreviousClick() {
    this.gantt.shiftPrevious();
}

onShiftNextClick() {
    this.gantt.shiftNext();
}

onStartDateChange({ value, oldValue }) {
    if (value) {
        this.gantt.startDate = DateHelper.add(value, -1, 'week');

        this.gantt.project.setStartDate(value);
    }
}

onProjectSelected({ record }) {
    this.gantt.project.load(record.url);
}

onFilterChange({ value }) {
    if (value === '') {
        this.gantt.taskStore.clearFilters();
    }
    else {
        value = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

        this.gantt.taskStore.filter({
            filters : task => task.name && task.name.match(new RegExp(value, 'i')),
            replace : true
        });
    }
}

onFeaturesClick({ source : item }) {
    const { gantt } = this;

    if (item.feature) {
        const feature = gantt.features[item.feature];
        feature.disabled = !feature.disabled;
    }
    else if (item.subGrid) {
        const subGrid = gantt.subGrids[item.subGrid];
        subGrid.collapsed = !subGrid.collapsed;
    }
    else if (item.toggleConfig) {
        gantt[item.toggleConfig] = item.checked;
    }
}

onFeaturesShow({ source : menu }) {
    const { gantt } = this;

    menu.items.map(item => {
        const { feature } = item;

        if (feature) {
            // a feature might be not presented in the gantt
            // (the code is shared between "advanced" and "php" demos which use a bit different set of features)
            if (gantt.features[feature]) {
                item.checked = !gantt.features[feature].disabled;
            }
            // hide not existing features
            else {
                item.hide();
            }
        }
        else if (item.subGrid) {
            item.checked = gantt.subGrids[item.subGrid].collapsed;
        }
    });
}

onSettingsShow({ source : menu }) {
    const
        { gantt }                                  = this,
        { rowHeight, barMargin, duration, radius } = menu.widgetMap;

    rowHeight.value = gantt.rowHeight;
    barMargin.value = gantt.barMargin;
    barMargin.max = (gantt.rowHeight / 2) - 5;
    duration.value = gantt.transitionDuration;
    radius.value = gantt.features.dependencies.radius ?? 0;
}

onRowHeightChange({ value, source }) {
    this.gantt.rowHeight = value;
    source.owner.widgetMap.barMargin.max = (value / 2) - 5;
}

onBarMarginChange({ value }) {
    this.gantt.barMargin = value;
}

onAnimationDurationChange({ value }) {
    this.gantt.transitionDuration = value;
    this.styleNode.innerHTML = `.b-animating .b-gantt-task-wrap { transition-duration: ${value / 1000}s !important; }`;
}

onDependencyRadiusChange({ value }) {
    this.gantt.features.dependencies.radius = value;
}

onCriticalPathsClick({ source }) {
    this.gantt.features.criticalPaths.disabled = !source.pressed;
}

// endregion
};

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

/**
 * @module StatusColumn
 */

/**
 * A column showing the status of a task
 *
 * @extends Gantt/column/Column
 * @classType statuscolumn
 */
class StatusColumn extends Column {

    static get $name() {
        return 'StatusColumn';
    }

    static get type() {
        return 'statuscolumn';
    }

    static get isGanttColumn() {
        return true;
    }

    static get defaults() {
        return {
            // Set your default instance config properties here
            field      : 'status',
            text       : 'Status',
            editor     : false,
            cellCls    : 'b-status-column-cell',
            htmlEncode : false,
            filterable : {
                filterField : {
                    type  : 'combo',
                    items : ['Not Started', 'Started', 'Completed', 'Late']
                }
            }
        };
    }

    //endregion

    renderer({ record }) {
        const status = record.status;

return status ? [{
    tag       : 'i',
    className : `b-fa b-fa-circle ${status}`
}, status] : '';
    }

// * reactiveRenderer() {
//     const
//         percentDone = yield this.record.$.percentDone;
//         //endDate     = yield this.record.$.endDate;
//
//     let status;
//
//     if (percentDone >= 100) {
//         status = 'Completed';
//     }
//     // else if (endDate < Date.now()) {
//     //     status = 'Late';
//     // }
//     else if (percentDone > 0) {
//         status = 'Started';
//     }
//
//     return status ? {
//         tag       : 'i',
//         className : `b-fa b-fa-circle ${status}`,
//         html      : status
//     } : '';
// }
}

ColumnStore.registerColumnType(StatusColumn);

// here you can extend our default Task class with your additional fields, methods and logic
class Task extends TaskModel {

static $name = 'Task';

static get fields() {
    return [
        'status' // For status column
    ];
}

get isLate() {
    return !this.isCompleted && this.deadlineDate && Date.now() > this.deadlineDate;
}

get status() {
    let status = 'Not started';

    if (this.isCompleted) {
        status = 'Completed';
    }
    else if (this.isLate) {
        status = 'Late';
    }
    else if (this.isStarted) {
        status = 'Started';
    }

    return status;
}
}

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

dependencyIdField : 'wbsCode',

selectionMode : {
    cell       : true,
    dragSelect : true,
    //rowNumber  : true
},

project : {
    // Let the Project know we want to use our own Task model with custom fields / methods
    taskModelClass : Task,
    transport      : {
        load : {
            url : '../_datasets/launch-saas.json'
        }
    },
    autoLoad  : true,
    taskStore : {
        wbsMode : 'auto'
    },
    // The State TrackingManager which the UndoRedo widget in the toolbar uses
    stm : {
        autoRecord : true
    },
    // Reset Undo / Redo after each load
    resetUndoRedoQueuesAfterLoad : true
},

startDate                     : '2019-01-12',
endDate                       : '2019-03-24',
resourceImageFolderPath       : '../_shared/images/users/',
scrollTaskIntoViewOnCellClick : true,

columns : [
    { type : 'check' },
    { type : 'wbs' },
    { type : 'name', width : 250 },
    { type : 'startdate' },
    { type : 'duration' },
    { type : 'resourceassignment', width : 120, showAvatars : true },
    { type : 'percentdone', showCircle : true, width : 70 },
    {
        type  : 'predecessor',
        width : 112
    },
    {
        type  : 'successor',
        width : 112
    },
    { type : 'schedulingmodecolumn' },
    { type : 'calendar' },
    { type : 'constrainttype' },
    { type : 'constraintdate' },
    { type : 'statuscolumn' },
    { type : 'deadlinedate' },
    { type : 'addnew' }
],

subGridConfigs : {
    locked : {
        flex : 3
    },
    normal : {
        flex : 4
    }
},

columnLines : false,

features : {
    baselines : {
        disabled : true
    },
    dependencyEdit : true,
    filter         : true,
    labels         : {
        left : {
            field  : 'name',
            editor : {
                type : 'textfield'
            }
        }
    },
    parentArea : {
        disabled : true
    },
    progressLine : {
        disabled   : true,
        statusDate : new Date(2019, 0, 25)
    },
    rollups : {
        disabled : true
    },
    rowReorder : {
        dropOnLeaf: true,
        gripOnly: true,
        showGrip : true
    },
    timeRanges : {
        showCurrentTimeLine : true
    },
    fillHandle    : true,
    cellCopyPaste : true,
    taskCopyPaste : {
        allowNativeClipboard : true
    }
},

tbar : {
    type : 'gantttoolbar'
}
});
Last edited by jit@flowit.dk on Fri Mar 31, 2023 2:17 pm, edited 1 time in total.

Best regards
Jimmy Thomsen


Post by tasnim »

Hi,

Thanks for reporting. We'll check what's wrong. Here is the ticket to track progress https://github.com/bryntum/support/issues/6495

Good Luck :),
Tasnim


Post by jit@flowit.dk »

Thank you, @tasnim

Best regards
Jimmy Thomsen


Post Reply