Our flexible Kanban board for managing tasks with drag drop


Post by harish22 »

Hi Team,
I'm using Bryntum TaskBoard in my Angular app. I’m handling task movement using the onTaskDrop listener:

onTaskDrop({ taskRecords, targetColumn, source, domEvent }) {
  // Need to find insert index here
}

Whenever I drop a task into a column, I want to know the exact insert index of the task relative to the other tasks already in that column.

For example:
If a column has 3 tasks and I drag a new task into the second position, how do I determine that the insert index is 1?

Is there any helper method or recommended approach to calculate the insert index based on domEvent or existing tasks in the targetColumn?

Thanks in advance!


Post by marcio »

Hi,

To find the insert index of a task in the onTaskDrop event, you can use the following snippet (presented on this demo - https://codepen.io/marciogurka/pen/qEdbNoL):

taskDrop: ev => {
      const [taskRecord] = ev.taskRecords;
      const index = ev.targetColumn.tasks.findIndex(task => task.id === taskRecord.id);
      console.log(`Task dropped on ${index} position`);
    }

Best regards,
Márcio

How to ask for help? Please read our Support Policy


Post by harish22 »

Thanks, Marcio — it works well!

I do have one more question regarding event cleanup in Bryntum TaskBoard within Angular. I'm trying to properly destroy event listeners in ngOnDestroy(), but using both .un() and .off() doesn’t seem to remove the events as expected.

Is there a recommended way to clean up TaskBoard events in Angular during component destruction?

async ngAfterViewInit() {
    this.taskBoard = this.taskBoardComponent.instance;
    this.project = this.projectComponent.instance;
    
// Initialize TaskBoard with empty state if (this.taskBoard) { this.taskBoard.on('taskDragStart', () => { this.taskApiService.pausePolling(); }); // Resume polling when drag ends this.taskBoard.on('taskDragEnd', () => { this.taskApiService.resumePolling(); }); // Also handle drag cancel this.taskBoard.on('taskDragAbort', () => { this.taskApiService.resumePolling(); }); } this.taskBoard.on("taskDrop", this.onTaskDrop.bind(this)); } ngOnDestroy() { if(this.taskBoard){ // this.taskBoard.un('taskDragStart'); // this.taskBoard.un('taskDragAbort'); // this.taskBoard.un('taskDragEnd'); } // Clean up subscriptions and polling this.subscription.unsubscribe(); this.taskApiService.stopPolling(); }

Post by marcio »

Hey harish22,

To properly clean up event listeners in Angular, you can use the detacher function returned by the on() method. When you add a listener, it returns a function that you can call to remove the listener. Here's how you can implement it:

async ngAfterViewInit() {
    this.taskBoard = this.taskBoardComponent.instance;
    this.project = this.projectComponent.instance;

    if (this.taskBoard) {
        this.detachTaskDragStart = this.taskBoard.on('taskDragStart', () => {
            this.taskApiService.pausePolling();
        });

        this.detachTaskDragEnd = this.taskBoard.on('taskDragEnd', () => {
            this.taskApiService.resumePolling();
        });

        this.detachTaskDragAbort = this.taskBoard.on('taskDragAbort', () => {
            this.taskApiService.resumePolling();
        });

        this.detachTaskDrop = this.taskBoard.on("taskDrop", this.onTaskDrop.bind(this));
    }
}

ngOnDestroy() {
    if (this.taskBoard) {
        this.detachTaskDragStart();
        this.detachTaskDragEnd();
        this.detachTaskDragAbort();
        this.detachTaskDrop();
    }
    // Clean up subscriptions and polling
    this.subscription.unsubscribe();
    this.taskApiService.stopPolling();
}

This approach ensures that all listeners are properly removed when the component is destroyed.

Documentation reference: https://bryntum.com/products/taskboard/docs/api/Core/mixin/Events#function-on

Best regards,
Márcio

How to ask for help? Please read our Support Policy


Post by harish22 »

Thanks again.

Even though I’ve already added the mentioned methods, I still occasionally encounter the following issue:

The entire screen freezes, and it requires a manual refresh to return to normal.

This doesn’t happen consistently—it occurs only sometimes.

I’ve attached the console errors and the relevant code snippet for reference.

I tried to reproduce the issue by quickly performing drag and drop operations in the UI. The freeze tends to happen when drag and drop is done rapidly across columns. It doesn't occur every time but happens intermittently under quick interactions.

onTaskDrop({ taskRecords, targetColumn, source }: any): void {
    // Add null checks for required parameters
    if (!taskRecords || !targetColumn || taskRecords.length === 0) {
      console.error('Invalid task drop: missing required parameters');
      return;
    }

// Ensure taskRecords is an array
if (!Array.isArray(taskRecords)) {
  console.error('Invalid task drop: taskRecords is not an array');
  return;
}

try {
  if (this.taskBoard) {
    this.taskBoard.mask('loading.........')
  }
  // Get all tasks in the target column, excluding the ones being moved (to avoid duplicates)
  const tasksInColumn = this.projectData.tasks
  .filter((t: any) => {
    // Add null checks for each property access
    if (!t || !t.technicianId) return false;
    if (!Array.isArray(taskRecords)) return false;
    
    return t.technicianId === targetColumn.id && 
           !taskRecords.some((tr: any) => tr?.dispatchId === t.dispatchId);
  })
  .filter((t: any) => t?.workAssignmentStatus !== 'C')
  .sort((a: any, b: any) => (a?.schedulerSequenceNum ?? 0) - (b?.schedulerSequenceNum ?? 0));
  
  // Update technician assignment in task list
  taskRecords.forEach(async (taskRecord: any) => {
    try {
        const insertIndex = targetColumn.tasks.findIndex((task : any) => task.dispatchId === taskRecord.dispatchId);
        const newSequenceNum = this.calculateNewSequenceNum(tasksInColumn, insertIndex, targetColumn.id);
        const taskIndex = this.projectData.tasks.findIndex(
        (t: any) => t.dispatchId === taskRecord.dispatchId
      );
      if (taskIndex !== -1) {
        // Update the task in the UI first
        this.projectData.tasks[taskIndex].technicianId = targetColumn.id;
        this.projectData.tasks[taskIndex].schedulerSequenceNum = newSequenceNum;
        //taskRecord.schedulerSequenceNum = newSequenceNum;
        // Make the API call to update the backend
        this.updateWorkAssignment(taskRecord, targetColumn);
        this.taskBoard.unmask();
      }
    } catch (error) {
      console.error('Error processing task record:', error);
      // Ensure the drag operation completes even if there's an error
      if (this.taskBoard) {
        this.taskBoard.resumeEvents();
        this.taskBoard.unmask();
      }
    }
    
  });
  if (this.taskBoard?.project?.taskStore) {
    const taskStore = this.taskBoard.project.taskStore as TaskStore;
    taskStore.sort('schedulerSequenceNum', true);
  }
  this.calculateTechnicianHours();
  
  // Update all column texts without rebuilding the structure
  if (this.taskBoard && this.taskBoard.columns) {
    this.taskBoard.columns.forEach((column: any) => {
      const resource = this.projectData.resources.find(
        (r: any) => r.technicianId === column.id
      );
      if (resource) {
        const totalMinutes = this.technicianHours[column.id] || 0;
        const formatted = this.formatHours(totalMinutes);
        column.text = `${resource.firstName} ${resource.lastName} (${formatted})`;
      }
    });
  } 
} catch (error) {
  console.error('Error processing task drop:', error);
  // Ensure the drag operation completes even if there's an error
  if (this.taskBoard) {
    this.taskBoard.resumeEvents();
  }
}
  }
private updateWorkAssignment(task: any, targetColumn: any): void {
    const currentUserId = localStorage.getItem('UserID') || '';
    const currentTask = this.projectData.tasks.find((t: any) => t.dispatchId === task.data.dispatchId);
    
if (!currentTask) return; let workAssignmentStatus = currentTask.workAssignmentStatus; let technicianId = targetColumn.id; // Scenario 1: Assigning to a Tech (from Unassigned) if (targetColumn.id !== 'UnAssigned') { workAssignmentStatus = 'A'; } // Scenario 2: Un-assigning from a Tech else if (targetColumn.id === 'UnAssigned') { workAssignmentStatus = 'U'; technicianId = null; } // Scenario 3: Re-assigning to another Tech // Keep the same workAssignmentStatus const payload: WorkAssignmentUpdatePayload = { dispatchId: task.data.dispatchId, workOrderId: currentTask.workOrderNumber, modifiedByUserId: currentUserId, ... }; try { // Update UI optimistically before API call if (this.taskBoard?.project?.taskStore) { this.taskBoard.suspendEvents(); const taskStore = this.taskBoard.project.taskStore as TaskStore; const taskToUpdate = taskStore.getById(task.data.dispatchId); if (taskToUpdate) { taskToUpdate.set({ technicianId: targetColumn.id, workAssignmentStatus: workAssignmentStatus }); } this.taskBoard.resumeEvents(); } // Update local data optimistically const taskIndex = this.projectData.tasks.findIndex((t: any) => t.dispatchId === task.data.dispatchId); if (taskIndex !== -1) { this.projectData.tasks[taskIndex] = { ...this.projectData.tasks[taskIndex], technicianId: targetColumn.id, workAssignmentStatus: workAssignmentStatus }; } // Recalculate hours and update columns optimistically this.calculateTechnicianHours(); this.updateTaskBoardColumns(); // Make the API call this.taskApiService.updateWorkAssignment(payload).subscribe({ next: () => { console.log("API called") }, error: (error) => { console.error('Error updating work assignment:', error); // Revert changes on error this.processTasks({ result: { workAssignments: [currentTask] } }); } }); } catch (error) { console.error('Error updating work assignment:', error); } }

Console Error:
Cannot read properties of undefined (reading 'forEach')
at TaskZone.dragMove (taskboard.module.js:96367:28)
at DragContext.updateTarget (taskboard.module.js:62309:14)
at DragContext.set (taskboard.module.js:3549:74)
at DragContext.updateTargetElement (taskboard.module.js:62323:26)
at DragContext.set (taskboard.module.js:3549:74)
at DragContext.track (taskboard.module.js:62509:21)
at DragContext.move (taskboard.module.js:62458:12)
at TaskZone.onDragPointerMove (taskboard.module.js:62930:38)
at HTMLBodyElement.handler (taskboard.module.js:8769:72)
at _ZoneDelegate.invokeTask (zone.js:409:31)
handleError @ core.mjs:6619Understand this error
3core.mjs:6619 ERROR TypeError: Cannot read properties of undefined (reading 'forEach')
at TaskZone.dragMove (taskboard.module.js:96367:28)
at DragContext.track (taskboard.module.js:62511:40)
at DragContext.move (taskboard.module.js:62458:12)
at TaskZone.onDragPointerMove (taskboard.module.js:62930:38)
at HTMLBodyElement.handler (taskboard.module.js:8769:72)
at _ZoneDelegate.invokeTask (zone.js:409:31)
at core.mjs:15428:55
at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:15428:36)
at _ZoneDelegate.invokeTask (zone.js:408:60)
at Object.onInvokeTask (core.mjs:15730:33)
handleError @ core.mjs:6619Understand this error
3core.mjs:6619 ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'some')
TypeError: Cannot read properties of undefined (reading 'some')
at hasChanged (taskboard.module.js:96055:53)
at taskboard.module.js:96411:131
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:22:1)
at asyncToGenerator.js:27:1
at new ZoneAwarePromise (zone.js:1432:21)
at asyncToGenerator.js:19:1
at TaskZone.dragDrop (taskboard.module.js:96517:6)
at taskboard.module.js:62705:18
at hasChanged (taskboard.module.js:96055:53)
at taskboard.module.js:96411:131
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:22:1)
at asyncToGenerator.js:27:1
at new ZoneAwarePromise (zone.js:1432:21)
at asyncToGenerator.js:19:1
at TaskZone.dragDrop (taskboard.module.js:96517:6)
at taskboard.module.js:62705:18
at resolvePromise (zone.js:1214:31)
at zone.js:1121:17
at zone.js:1137:33
at asyncGeneratorStep (asyncToGenerator.js:6:1)
at _throw (asyncToGenerator.js:25:1)
at _ZoneDelegate.invoke (zone.js:375:26)
at Object.onInvoke (core.mjs:15743:33)
at _ZoneDelegate.invoke (zone.js:374:52)
at Zone.run (zone.js:134:43)
at zone.js:1278:36


Post by marcio »

Hey harish22,

The issue you're experiencing might be related to asynchronous operations or race conditions during rapid drag-and-drop actions. Here are a few suggestions to help address the problem:

  • Ensure Proper Error Handling: Make sure all asynchronous operations, such as API calls, are properly handled with try-catch blocks and promise handling to prevent unhandled exceptions.

  • Optimize Event Handling: Consider debouncing or throttling the drag-and-drop operations to prevent rapid consecutive actions that might lead to race conditions.

  • Check for Undefined Values: The errors indicate that some properties are undefined. Ensure that all objects and arrays are properly initialized and checked before accessing their properties.

  • Use suspendEvents and resumeEvents Carefully: Ensure that events are resumed correctly after suspension, especially if multiple operations are involved.

  • Console Logs and Debugging: Add console logs to trace the flow of operations and identify where the undefined values might be occurring.

If the issue persists, please provide a minimal reproducible example to help diagnose the problem further.

Best regards,
Márcio

How to ask for help? Please read our Support Policy


Post Reply