Cannot read properties of undefined (reading '$local_index')

Heyo,
when parsing data into the Gantt, I sometimes get the following error:

ganttHelper.ts:60 TypeError: Cannot read properties of undefined (reading '$local_index')
    at ./sources/ext/undo/monitor.ts.Monitor.onTaskMoved (dhtmlxgantt.js:56776:48)
    at TreeDataStore.<anonymous> (dhtmlxgantt.js:56955:23)
    at TreeDataStore.eventStorage (dhtmlxgantt.js:60230:39)
    at obj.callEvent (dhtmlxgantt.js:60312:63)
    at TreeDataStore.move (dhtmlxgantt.js:21119:10)
    at TreeDataStore.$initItem (dhtmlxgantt.js:20813:12)
    at TreeDataStore._parseInner (dhtmlxgantt.js:19570:21)
    at TreeDataStore.parse (dhtmlxgantt.js:20958:23)
    at gantt._process_loading (dhtmlxgantt.js:23262:27)
    at gantt.on_load (dhtmlxgantt.js:23232:10)
    at gantt.parse (dhtmlxgantt.js:23174:10)
    at convertOrdersWithProjectsWithJobsIntoTasksAndParse (ganttHelper.ts:53:13)
    at Gantt.svelte:553:5

With only a few tasks, this happens very rarely, but once I get into the 1000s of tasks, it becomes a constant pain. Currently I am forced to do the following:

  for (let i = 0; i < numRetries; i++) {
    try {
      gantt.parse({
        data: allData.tasks,
      });
      console.log('Retry tasks number: ' + i);
      break;
    } catch (e) {
         ...
    }
  }

The amount of retries necessary until it works, are random. Sometimes it works on the first or second retry, sometimes it takes a 150 iterations. This obviously adds a considerable overhead, taking up to 10 seconds.
My next step would be to turn every parsing into batches (I’ve already separated parent and children parsing), but this again would be slower than parsing all data at once.

Hello Hendrik,
According to the stack trace, you have the undo extension enabled and you load the data when Gantt is already loaded. When you load tasks, Gantt detects that there are some tasks that have a different value in the parent parameter comparing to the existing dataset. So, it tries to move the newly loaded task to the position of the parent task.

I tried to reproduce the issue in the following snippet, but it is working correctly:
https://snippet.dhtmlx.com/ywohia4c

The issue is only reproduced when I specify the non-existing parent ID:
https://snippet.dhtmlx.com/01f76a79

In that case, the task disappears, and it is expected behavior. But you shouldn’t get an error when the undo extension is enabled.
However, if the task has a new parent and it is in the dataset, the issue is not reproduced.

I added it as a bug to our internal bug tracker. The dev team will fix it in the future, but I cannot give you any ETA.

To not get an error, you need to make sure that the parent task is present in the existing or new dataset.

If you have a different use case, it is hard to suggest what might be wrong as I don’t see your code. In that case, please add your configuration to the following snippet and make sure that the issue is reproduced there:

Then, click on the Save button and send me the link.
Or send me a ready demo with all the necessary JavaScript and CSS files so that I can reproduce the issue locally.

Hello ramil, thank you for your response.
I’ve added gantt.clearAll(); and still get the same behaviour, so the problem should not be happening due to already existing tasks?

Disabling the undo extension, however, seems to fix the issue.
This is our current Gantt configuration (undo should be enabled, of course):

      gantt.plugins({
        export_api: true,
        keyboard_navigation: true,
        undo: false,
        tooltip: true,
        multiselect: true,
        marker: true,
        drag_timeline: true,
      });
      gantt.config.keyboard_navigation_cells = true;
      gantt.config.show_links = false;
      gantt.config.drag_move = true;
      gantt.config.drag_resize = false;
      gantt.config.multiselect = true;
      gantt.config.show_progress = false;
      gantt.config.drag_timeline = {
        ignore: '.gantt_task_line, .gantt_task_link',
        useKey: false,
      };
      gantt.config.details_on_dblclick = false;
      gantt.config.lightbox = false;
      gantt.config.undo = false;
      gantt.config.redo = true;

      gantt.templates.tooltip_text = function (start, end, task) {
        if (task.projectIds) {
          return `<b>${translate('order').order}:</b> ${task.text}`;
        } else if (task.taskIds) {
          return `<b>${translate('project').project}:</b> ${task.text}`;
        } else {
          return `<b>${translate('job').job}:</b> ${task.text} <br> <b>Typ:</b> ${task.typeName}`;
        }
      };

      gantt.ext.zoom.init(getZoomConfig(gantt));
      gantt.ext.zoom.setLevel($ganttMode);

      gantt.config.start_date = new Date($ganttDate);
      gantt.config.end_date = new Date(addDays($ganttDate, getNumberOfDaysForMode($ganttMode)));

      gantt.config.columns = [
        { name: 'text', label: 'Name', width: 180, tree: true, resize: true },
        {
          name: 'start_date',
          label: 'Start Datum',
          width: 80,
          align: 'center',
          template: function (task) {
            return getDayMonthYearFrom(task.start_date || new Date());
          },
        },
        {
          name: 'end_date',
          label: 'End Datum',
          width: 80,
          align: 'center',
          template: function (task) {
            return getDayMonthYearFrom(task.end_date || new Date());
          },
        },
        {
          name: 'isLocked',
          label: '',
          width: 64,
          align: 'center',
          template: function (task) {
            if (task.isLocked) {
              return `<i class="ri-lock-2-fill" data-action style="cursor: pointer;"></i>`;
            } else if (!task.isLocked) {
              return `<i class="ri-lock-unlock-line" data-action style="cursor: pointer;"></i>`;
            }
          },
        },
      ];

      if (clientOverwrites.gantt.hours) {
        gantt.config.columns.splice(3, 0, { name: 'hours', label: 'Stunden', width: 64, align: 'center' });
      }

Hello Hendrik,
Do you have the same stack trace after adding the clearAll method before the parse method? If it is the same, it is hard to suggest what might be wrong as I don’t have your full configuration.
I tried adding the configuration you shared to the following snippet, but the issue is not reproduced:
https://snippet.dhtmlx.com/1fhefbsv

If the stack trace is different, probably, it is related to something else or you may have a custom code that changes the tree level of tasks after loading them.

I just double checked, and in my original code the clearAll was already in place, right before doing the parsing, so the stack trace is the same (it was before calling the method whose code I posted at first, that’s why it’s not part of the post).

I’ll see if I can piece together more of the relevant code.