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.
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:
targetSelectorThis is a CSS selector defining what elements can be draggedcloneTargetWe set this totrueto clone the mouse-down element, and not move the actual grid row.autoSizeClonedTargetWe size the drag proxy manually using CSSunifiedProxyWhen dragging multiple items, we collect all extra element drag proxies and place them below the main dragged elementproxySelectorInstead of dragging the full row element, we only drag the avatar for a nice user experience.dropTargetSelectorThis is a CSS selector defining where drops are allowed, and we set this to.b-gantt .b-grid-sub-gridwhich targets any of the Gantt's sub grids (the locked tree section or the timeline section)callOnFunctionsBy setting this totrue, 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: