Urgent: Error when resizing rows on custom datastore

When setting gantt.config.resize_rows = true and resizing the custom datastore bind in the grid, it will show an error. Please refer to the snippet below.

https://snippet.dhtmlx.com/31texfm5

P.S. using professional license

Hello Cairo,
The resize rows feature expects to have the task object, so the same error occurs even if you try to resize the resource rows, and there is no task with the same ID:
https://snippet.dhtmlx.com/l63vv124

The dev team will fix the bug in the future, but I cannot give you any ETA.

As a workaround, you can add the IDs of existing tasks to the custom datastore items:
https://snippet.dhtmlx.com/ucdnz9ut

Hi Ramil,

Thanks for the reply. That works somehow but when I have a “treeDatastore” and then resizing the parent, it errors the same. Now, when resizing the children, the tree is broken, and no open/close tree is shown. I also have a groupby.

Roughly this is the code:

 /**
   * Create the grouping for the gantt chart.
   */
  function createGrouping() {
    const gantt = getGantt();
    if (!gantt) return;

    gantt.plugins({ grouping: true });

    // Clone the task for each result resource.
    gantt.batchUpdate(() => {
      const clonedTaskIds: any[] = [];
      let currentTaskId = 1;

      gantt.eachTask((task: JobTask) => {
        // Skip if the task is not a job.
        if (!task.id.toString().startsWith(JOB_TASK_PREFIX)) {
          return;
        }

        // If the task has no result resource, delete it.
        if (!task.result_resource || task.result_resource.length === 0 || task.unscheduled) {
          gantt.deleteTask(task.id);
          return;
        }

        // Create a resource job task for each resource for the job.
        for (let i = 0; i < task.result_resource.length; i++) {
          const resourceId = task.result_resource[i].resource_id;
          const resourceName = getLabelFromList(gantt.serverList(GANTT_SERVER_LIST_RESOURCE), resourceId);
          const resourceGroupId = task.result_resource[i].piled_resource_group_id;

          // Skip if the resource name is empty.
          if (!resourceName) {
            return;
          }

          const resourceJob: ResourceJobTask = {
            id: currentTaskId++,
            text: resourceName,
            original_job_task_id: task.id,
            job_name: task.text,
            unscheduled: task.unscheduled,
            start_date: task.start_date,
            end_date: task.end_date,
            is_fixed: task.is_fixed,
            memo: task.memo,
            progress: task.progress,
            resource_type: getTypeFromList(gantt.serverList(GANTT_SERVER_LIST_RESOURCE), resourceId),
            resource_id: resourceId,
            resource_quantity: task.result_resource[i].resource_quantity || 1,
            is_resource_job: true,
            $temporary: true,
            piled_resource_group_id: resourceGroupId ? [resourceGroupId] : undefined
          };

          gantt.addTask(resourceJob);
          clonedTaskIds.push(resourceJob.id);
        }
      });

      // Group the tasks by the resource group.
      gantt.groupBy({
        groups: gantt.serverList(GANTT_SERVER_LIST_RESOURCE_GROUP),
        relation_property: 'piled_resource_group_id',
        group_id: 'key',
        group_text: 'label',
        default_group_label: GANTT_DEFAULT_GROUP_TEXT
      });

      cleanUpUnusedTasks();
      recreateLinks(clonedTaskIds);
      createPilingItems();
      sortResourceGantt();
    });
  }

  /**
   * Recreate the links for the cloned tasks.
   */
  function recreateLinks(clonedTaskIds: string[]) {
    const gantt = getGantt();
    if (!gantt) return;

    // Fetch all existing links
    const originalLinks = gantt.getLinks();

    // Create a mapping of original task IDs to cloned task IDs
    const idMap = new Map<string, string>();
    clonedTaskIds.forEach((taskId) => {
      const clonedTask = gantt.getTask(taskId);
      if (clonedTask && clonedTask.original_id) {
        idMap.set(clonedTask.original_id, clonedTask.id as string);
      }
    });

    // Recreate the links using the idMap
    originalLinks.forEach((link) => {
      const newSource = idMap.get(link.source as string);
      const newTarget = idMap.get(link.target as string);

      if (newSource && newTarget) {
        // Both source and target are cloned, recreate the link
        gantt.addLink({
          source: newSource,
          target: newTarget,
          type: link.type
        });
      } else if (newSource) {
        // Only the source is cloned
        gantt.addLink({
          source: newSource,
          target: link.target,
          type: link.type
        });
      } else if (newTarget) {
        // Only the target is cloned
        gantt.addLink({
          source: link.source,
          target: newTarget,
          type: link.type
        });
      }
    });
  }

  /**
   * Clean up the unused tasks.
   */
  function cleanUpUnusedTasks() {
    const gantt = getGantt();
    if (!gantt) return;

    gantt.eachTask((task: Task) => {
      if (task.id.toString().startsWith(ITEM_TASK_PREFIX) || task.id.toString().startsWith(ORDER_TASK_PREFIX)) {
        gantt.deleteTask(task.id);
        return;
      }

      task.parent = task.$rendered_parent;
      gantt.close(task.id);

      if (task.id.toString().startsWith(JOB_TASK_PREFIX)) {
        gantt.deleteTask(task.id);
        return;
      }
    });
  }

  /**
   * Parse the piling items for the resource gantt chart.
   */
  function createPilingItems() {
    const gantt = getGantt();
    if (!gantt) return;

    const pilingItems: any[] = [];
    const processedResourceGroups = new Set<string | number>();
    const processedResources = new Set<string | number>();

    gantt.eachTask((task: Task) => {
      if (task.$level === ResourceGanttLayer.ResourceGroup) {
        const resourceGroupTask = task;
        const taskId = task.id;
        const resourceGroupId = Number(task.key);

        if (!processedResourceGroups.has(taskId)) {
          const resourceGroupItem = {
            id: taskId,
            piled_resource_group_id: resourceGroupId,
            parent: gantt.config.root_id,
            text: resourceGroupTask.text,
            resource_type: getTypeFromList(gantt.serverList(GANTT_SERVER_LIST_RESOURCE_GROUP), resourceGroupId),
            row_height: settings?.dhtmlx?.histogramDefaultHeight || GANTT_DEFAULT_PILING_HEIGHT,
            capacity: GANTT_MAX_CAPACITY_HEIGHT
          };

          pilingItems.push(resourceGroupItem);
          processedResourceGroups.add(taskId);
        }
      }

      if (task.$level === ResourceGanttLayer.Resource) {
        const resourceTask = task;
        const taskId = task.id;
        const resourceGroupId = task.piled_resource_group_id === 0 ? task.piled_resource_group_id : Number(task.piled_resource_group_id[0]);
        const parentTaskId = task.parent;
        const resourceId = task.resource_id;

        const resourceGroupKey = `${resourceGroupId}-${resourceId}`;

        if (!processedResources.has(resourceGroupKey)) {
          const resourceItem = {
            id: taskId,
            parent: parentTaskId,
            resource_id: resourceId,
            text: resourceTask.text,
            resource_type: resourceTask.resource_type,
            resource_quantity: resourceTask.resource_quantity,
            row_height: settings?.dhtmlx?.histogramDefaultHeight || GANTT_DEFAULT_PILING_HEIGHT,
            capacity: GANTT_MAX_CAPACITY_HEIGHT
          };

          pilingItems.push(resourceItem);
          processedResources.add(resourceGroupKey);
        }
      }
    });

    gantt.config.resource_store = GANTT_PILING_STORE;
    gantt.createDatastore({
      name: GANTT_PILING_STORE,
      type: 'treeDatastore'
    });

    const pilingStore = gantt.getDatastore(GANTT_PILING_STORE);
    pilingStore.parse(pilingItems);
    pilingStore.sort('resource_type', false);
  }

  /**
   * Sort the resource gantt chart by the resource type and start date.
   */
  function sortResourceGantt() {
    const gantt = getGantt();
    if (!gantt) return;

    gantt.sort(function (a: Task, b: Task) {
      if (a.$level === ResourceGanttLayer.ResourceGroup && a.text === GANTT_DEFAULT_GROUP_TEXT) {
        return 1;
      }

      if (b.$level === ResourceGanttLayer.ResourceGroup && a.text === GANTT_DEFAULT_GROUP_TEXT) {
        return -1;
      }

      if (
        a.$level === ResourceGanttLayer.Resource &&
        b.$level === ResourceGanttLayer.Resource &&
        a.piled_resource_group_id === 0 &&
        b.piled_resource_group_id === 0
      ) {
        if (a.resource_type < b.resource_type) {
          return -1;
        }

        if (a.resource_type > b.resource_type) {
          return 1;
        }
      }

      if (a.start_date && b.start_date) {
        if (a.start_date < b.start_date) return -1;
        if (a.start_date > b.start_date) return 1;
      }

      return 0;
    });
  }

This is my config.

gantt.config.readonly = true;
    gantt.config.scale_height = 100;
    gantt.config.round_dnd_dates = false;
    gantt.config.duration_unit = 'minute';
    gantt.config.duration_step = 1;
    gantt.config.time_step = 1;
    gantt.config.min_duration = 0;
    gantt.config.date_format = TASK_DATETIME_FORMAT;
    gantt.config.bar_height = 30;
    gantt.config.server_utc = true;
    gantt.config.show_errors = false;
    gantt.config.initial_scroll = false;
    gantt.config.resource_property = 'result_resource';
    gantt.config.process_resource_assignments = false;
    gantt.config.work_time = true;
    gantt.config.dynamic_resource_calendars = true;
    gantt.config.resource_render_empty_cells = true;
    gantt.config.deepcopy_on_parse = true;
    gantt.config.resize_rows = true;

Hello Cairo,
It should work the same way even when there are tree levels:
https://snippet.dhtmlx.com/0v03jvh4

If it doesn’t work, most likely, you don’t have the same IDs as the existing task IDs. So, another workaround is to add fake tasks to the task datastore before the resize and remove them after the resize.
Here is the code for the resource grid:

const fakeTasks = [];

gantt.getLayoutView("resourceGrid").attachEvent("onBeforeRowResize", function (item) {
    const taskStore = gantt.getDatastore("task")
    if (!taskStore.pull[item.id]){
        taskStore.pull[item.id] = {id: item.id};
        fakeTasks.push(item.id)
    }
    return true;
});

gantt.getLayoutView("resourceGrid").attachEvent("onAfterRowResize", function (id, item, oldHeight, newHeight) {
    const taskStore = gantt.getDatastore("task")
    if (fakeTasks.length){
        delete taskStore.pull[fakeTasks.pop()]
    }
});

The resize rows feature is not supposed to work for the resource and custom grids, so, you can disable it:

gantt.getLayoutView("resourceGrid").attachEvent("onBeforeRowResize", function (item) {
    gantt.message(`Start resizing`);
    return false;
});
1 Like

That works perfectly Ramil. Thanks.