v7.3.0
SupportExamplesFree Trial

Assigning resources to tasks by dragging from an external grid

Intro

Assigning resources can be time-consuming if you are working with a large project. In Bryntum Gantt, we have a cool demo showing how to assign resources by dragging them from an external Grid component onto a task.

Please note, the Grid component is licensed separately from the Gantt component.

This makes resource assignment super easy and in this guide we will show you how the drag-resources-from-grid example was built.

Creating the Gantt and Grid components

In the demo we refer to, there are two main components - the Gantt and the resource Grid. These are both added to a Container widget, which serves as the main outer widget.

const container = new Container({
    appendTo : 'container',
    items    : {
        gantt : {
            type                    : 'gantt',
            project,
            flex                    : 1,
            resourceImageFolderPath : '../_shared/images/users/',
            subGridConfigs : {
                ...
            },
            columns : [
                { type : 'name', minWidth : 200 },
                { type : 'resourceassignment', width : 200, showAvatars : true },
                { type : 'startdate' },
                { type : 'duration' }
            ]
        },
        splitter : { type : 'splitter' },
        resourceGrid : {
            // The type is defined in the ./lib/ResourceGrid.js class
            type  : 'resourcegrid',
            // Gantt stores are contained by a project, pass the resourceStore to the grid to allow it to access resources
            store : project.resourceStore
        }
    }
});

The container lays out its children using basic flex box CSS.

#container > .b-container {
    flex-flow : row;
}

The Gantt and the Grid are separated by a Splitter to allow resizing the elements.

Configuring the DragHelper

With the two main components visible on the page, it is time to start thinking about the dragging part. The DragHelper provides all the drag functionality needed to allow dragging a resource from the grid and drop it on a Gantt task row.

In the demo, the Drag functionality is encapsulated in a custom demo subclass of DragHelper. First, a simple skeleton with some basic configuration is added.

import DragHelper from '../../../lib/Core/helper/DragHelper.js';
import Toast from '../../../lib/Core/widget/Toast.js';

export default class Drag extends DragHelper {
    static get configurable() {
        return {
            // This enables a simpler version of listening to events, providing `onDragStart` instead of listening for `dragStart` event
            callOnFunctions      : true,

            // Don't drag the actual row element, we clone an image instead
            cloneTarget          : true,

            // We size the cloned element using CSS
            autoSizeClonedTarget : false,

            // Stack all dragged DOM nodes below the main element
            unifiedProxy         : true,

            // Only allow drops on gantt task bars
            dropTargetSelector   : '.b-gantt-task-wrap,.b-gantt .b-grid-row',

            // Allow drag of row elements inside the resource grid
            targetSelector       : '.b-grid-row',

            // Drag just the avatar, not the full row
            proxySelector        : '.b-resource-avatar',

            // The app will provide references to the Gantt and the Grid instances
            gantt                : null,
            grid                 : null
        };
    }

    // Drag start callback 
    onDragStart({ context }) {
       // TODO
    }

    // Drag callback called every mouse move
    onDrag({ event, context }) {
       // TODO
    }

    // Drop callback after a mouse up, take action and assign the dragged resource(s) to the task (if the drop valid)
    onDrop({ context }) {
       // TODO
    }

    // Drop callback after a mouse up, take action and transfer the unplanned task to the real EventStore (if it's valid)
    onDropFinalized({ context }) {
        // TODO
    }
}

Above, the following configurations are used:

  • targetSelector This is a CSS selector defining what elements can be dragged
  • cloneTarget We set this to true to clone the mouse-down element, and not move the actual grid row.
  • autoSizeClonedTarget We size the drag proxy manually using CSS
  • unifiedProxy When dragging multiple items, we collect all extra element drag proxies and place them below the main dragged element
  • proxySelector Instead of dragging the full row element, we only drag the avatar for a nice user experience.
  • dropTargetSelector This is a CSS selector defining where drops are allowed, and we set this to .b-gantt .b-grid-sub-grid which targets any of the Gantt's sub grids (the locked tree section or the timeline section)
  • callOnFunctions By setting this to true, we will receive onXXX (e.g. onDragStart / onDrop) callbacks which is an easier way of listening for events

Processing drag start

A drag is deemed started when the pointer has moved more than the configured dragThreshold (defaults to 5px). At this time the dragStart event is fired and if using callOnFunctions, the onDragStart callback is called.

In our demo, at this stage we use the getRecordFromElement method to look up the resource record. If multiple rows are selected, we assign those elements to the relatedElements of the context object provided.

onDragStart({ context }) {
    const
        { grid, gantt } = this,
        { grabbed }     = context;

    // Lookup the resource and save a reference to it so we can access it later
    context.resourceRecord = grid.getRecordFromElement(grabbed);
    // If user selected multiple rows, let the drag helper know about those too
    context.relatedElements = Array.from(grid.element.querySelectorAll(`.b-selected:not([data-id="${grabbed.dataset.id}"])`));

    // If mouse move close to the sides of the Gantt time axis sub grid, trigger scrolling 
    gantt.enableScrollingCloseToEdges(gantt.timeAxisSubGrid);
}

Validating drag actions

In our demo, the main objective in the onDrag callback is to validate the drop. For a resource assignment to be valid, the resource cannot be already assigned to the task. We use the context object provided to every event / callback, which has a valid boolean which we set to signal drop validity. If the drop position is invalid, a special b-drag-invalid CSS class is added to the dragged element which you can use to style it to let the user know.

onDrag({ context, event }) {
    const targetTask = context.taskRecord = this.gantt.resolveTaskRecord(event.target);

    context.valid = Boolean(targetTask && !targetTask.resources.includes(context.resourceRecord));
}

A few styles applied to the currently hovered task row and invalid styling of the drag proxy.

.b-dragging-resource .b-grid-row:hover {
    background : #dddddd33;
}

.b-resource-avatar {
    &.b-drag-invalid {
         background : #D50000;
    }
}

Processing the drop

When a valid drop happens, we do not process the data transfer immediately. Instead we first animate the dragged element close to its final position which is the resource column cell which has a DIV containing avatars of all assigned resources.

This is done by calling the animateProxyTo method on the DragHelper. We specify which target element to transition the dragged element to, and how it should align to it. This method returns a Promise that we await, and it resolves when the transition is completed. At this point, dropFinalized event is also fired.

If the drop position is not valid (i.e. resource already assigned), a small toast message is shown.

After the transition has completed, it is time to do the data transfer. As you can see below, we iterate all selected resource records in the grid and assign the ones not already assigned to the task.

async
onDrop({ context }) {
    const
        { gantt, grid }                = this,
        { taskRecord, resourceRecord } = context;

    if (context.valid) {
      // Valid drop, provide a point to animate the proxy to before finishing the operation
      const
          resourceAssignmentCell = gantt.getCell({
            column: gantt.columns.get('assignments'),
            record: taskRecord
          }),
          avatarContainer        = resourceAssignmentCell.querySelector('.b-resource-avatar-container');

      // Before we finalize the drop and update the task record, transition the element to the target point
      if (avatarContainer) {
          await this.animateProxyTo(avatarContainer, {
            align: 'l0-r0'
          });
      }

      grid.selectedRecords.forEach(resourceRecord => {
          if (!taskRecord.resources.includes(resourceRecord)) {
            taskRecord.assign(resourceRecord);
          }
      });
    }
    else if (taskRecord?.resources.includes(resourceRecord)) {
          Toast.show(`Task is already assigned to ${resourceRecord.name}`);
    }

    gantt.element.classList.remove('b-dragging-resource');
    gantt.disableScrollingCloseToEdges(gantt.timeAxisSubGrid);
}

The final result

Here is a small video showing the a valid single drop, but also how the validation changes the border to red and aborts the drop in case the resource is already assigned to the task.

Learn more…

Want to learn more about what can be done relating to drag drop with the Bryntum SDKs? Please see these resources:

Contents