Custom component return data to retool app

Probably overlooking something simple, but haven't been able to work it out yet via the docs or searching this forum.

I have a Gantt Chart in a custom component that I imported from GitHub - frappe/gantt: Open Source JavaScript Gantt

The chart looks like this:

I am trying to add some code to send the changes made in the chart back to my retool app (and in future- save to MySQL), to do this I use this JS:

here's the iframe code for Gantt chart:

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.css">
<style>html {
    margin: -10px;
}</style>
<svg id="gantt"></svg>

<script>
window.Retool.subscribe(function (model) {
	var gantt = new Gantt('#gantt', model.tasks, {
		on_date_change: (task, start, end) => {
			save(end);
		},
		language: 'en'
	});
});


async function save(data) {
  window.Retool.modelUpdate({ 'changed': data });
  window.Retool.triggerQuery('saveChange');
}


</script>

it uses this model

{ 
 tasks: {{ getTasks.data }} 
}

here are some sample tasks data:

{
    "tasks": [
        {
            "id": "task-1",
            "name": "design a table",
            "start": "2023-10-25T00:00:00.000Z",
            "end": "2023-10-26T00:00:00.000Z",
            "progress": 10,
            "dependencies": "task-1",
            "custom_class": "bar-milestone"
        },
        {
            "id": "task-2",
            "name": "import node data",
            "start": "2023-10-26T00:00:00.000Z",
            "end": "2023-10-27T00:00:00.000Z",
            "progress": 10,
            "dependencies": "task-1",
            "custom_class": "bar-milestone"
        }
    ]
}

this almost works - when I change a task end date on the Gantt - I see the console log the message:

"save this change Thu Oct 26 2023 00:00:00 GMT+0800 (Australian Western Standard Time) Sun Oct 29 2023 23:59:59 GMT+0800 (Australian Western Standard Time"

which is great, HOWEVER, the Gantt chart doesn't persist the change, eg. the task slider returns to the default.

if I comment out the code that saves the data:

    //save(end)

now the chart works eg. dragging the task end date persists (it just isn't saved now):

I have tried returning null etc. from the save function, tried setting it to async, I've searched the forums and internet for solutions but I cant get it to work correctly,

Does someone know what I am doing wrong (I know JS but I am new to retool)

these pages got me close:

and, I'll keep looking...

Hi @coda1024

Every time you update the Model, the subscribe function get called so the Gantt as well.
In your case the subscribe get called on modelUpdate but the Gantt needs tasks and you're passing changed.
You need to be sure that after the query success, the model.tasks reflects the updated data model.

Hope this help

Thankyou for that, I have made changes to save the data to MySQL so the model will contain the correct data when it reloads

I updated the save function to include the data required by the MySQL update

function save(id, name, data) {

  const changed = {
    id: id,
    name: name,
    data: data
  };

  window.Retool.modelUpdate({ 'changed': changed });
  window.Retool.triggerQuery('saveChange2');
}

which updates this table

SET @id = {{customComponent1.model.changed.id}};
SET @name = {{customComponent1.model.changed.name}};
SET @data = {{customComponent1.model.changed.data}};


UPDATE Projects_Tasks
SET 
    TaskStartDate = CASE WHEN @name = 'start' THEN @data ELSE TaskStartDate END,
    TaskEndDate = CASE WHEN @name = 'end' THEN @data ELSE TaskEndDate END
WHERE 
    ProjectsTaskID = REPLACE(@id, 'task-', '');



this works, the date changed as expected.

for example

here's a sample task:

image

this task is represented in the Model as

{
    id: "task-3",
    name: "step3",
    start: "2023-10-01T00:00:00.000Z",
    end: "2023-10-04T00:00:00.000Z",
    progress: 80,
    dependencies: 'task-1'
}

when the end date is dragged to 10-Oct

{id: 'task-3', name: 'end', data: '2023-10-10'}

database updates OK

image

In Event Handlers, I trigger the query that updates the Model,

image

which persists the change on the chart:

image

nice.

next problem

however, there's the added side-effect of cascading Model updates - each time an update is triggered it doubles the number of updates - 1,2,4,8... updates per change (can triggers be overwritten or deleted?)

I will keep reading...

Any ideas are appreciated.Thanks!

Hi @coda1024

you need to handle it.
Usually I add an additional field, such as timestamp in the modelUpdate and check the value in the subscribe call, in order to skip or not the Gantt update.

Hope this help

1 Like

Thanks for your suggestion, I implemented it as follows:

I added a timestamp to the model (initialized with 0)

{ 
  tasks: {{ getTasks.data }}, 
  timestamp: 0
}

and updated the Model code so it sets the timestamp of the modelUpdate

  window.Retool.modelUpdate({ 'changed': changed, 'timestamp': Date.now() });

then on the subscribe call, check the timestamp

  if ( model.timestamp == 0 || model.timestamp > Date.now() ) {
  }

the final working custom component iframe code is:

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.css">
<style>html {
    margin: -10px;
}</style>
<svg id="gantt"></svg>

<script>
window.Retool.subscribe(function (model) {
  
  if (model.timestamp == 0 || model.timestamp > Date.now() ) {
  
  var gantt = new Gantt('#gantt', model.tasks, {
    on_date_change: (task, start, end) => {
      
      save(task.id, 'start', start);
      save(task.id, 'end', end);

    },
    language: 'en'
  });
  }
});



function save(id, name, data) {

  const changed = {
    id: id,
    name: name,
    data: data
  };

  window.Retool.modelUpdate({ 'changed': changed, 'timestamp': Date.now() });
  window.Retool.triggerQuery('saveChange');
}


</script>

Thanks so much four your help, you deserve a beer!

It has been fun learning about Models too

~cheers

update: after refreshing the page, its not working as expected, I will update this comment once I debug the issue.

Good to hear, and please mark the solution, this way can be more easy for other with similar issues.

Best

After some testing, I ended up going with a slightly different idea - using a debounce design pattern, which eliminates the need for a timestamp in the model since the timer is used in the iframe code.

What this does is, it waits for a pause in the rapid succession of on_date_change events. If another event comes in within 300ms, the previous one is discarded. If no new event comes in after 300ms, the save function is called. This will greatly reduce the number of calls to the save function.

It'll ensure that a function doesn't get called more than once within a specific timeframe, here I am using 300ms which works well in my app. This 300ms delay is arbitrary and can be adjusted based on the frequency of events and the desired delay.

My final version:

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.css">
<style>html {
    margin: -10px;
}</style>
<svg id="gantt"></svg>

<script>
let debounceTimer;

window.Retool.subscribe(function (model) {
  
  if (!model.changed) { 
    var gantt = new Gantt('#gantt', model.tasks, {
      on_date_change: (task, start, end) => {
        debouncedSave(task.id, 'start', start);
        debouncedSave(task.id, 'end', end);
      },
      language: 'en'
    });
  }
});

function debouncedSave(id, name, data) {
  clearTimeout(debounceTimer); // Clear any existing timer
  debounceTimer = setTimeout(() => save(id, name, data), 300); // Set a delay of 300ms (adjust as needed)
}

function save(id, name, data) {
  const changed = {
    id: id,
    name: name,
    data: data
  };

  window.Retool.modelUpdate({ 'changed': changed });
  window.Retool.triggerQuery('saveChange2');
}

</script>

~coda1024

1 Like

here is a fix for the version above - it has a bug where using a single timer for two debouncedSave functions can clear both timers, preventing one of them from saving.

also, theres some custom css for the mygroup class

<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/frappe-gantt/0.6.1/frappe-gantt.css">
<style>
.bar-mygroup .bar-label{
  font-size: 1.2em;
  font-weight: bold;
}

.bar-mygroup .bar{
  fill: orange;
}

</style>
<svg id="gantt"></svg>

<script>
let debounceTimers = {};

window.Retool.subscribe(function (model) {
  
  if (!model.changed) { 
    var gantt = new Gantt('#gantt', model.tasks, {
      on_date_change: (task, start, end) => {
        debouncedSave(task.id, 'start', start);
        debouncedSave(task.id, 'end', end);
      },
      language: 'en'
    });
  }
});

function debouncedSave(id, name, data) {
  const key = `${id}-${name}`;
  if (debounceTimers[key]) {
    clearTimeout(debounceTimers[key]); // Clear any existing timer for this specific id-name combo
  }
  
  debounceTimers[key] = setTimeout(() => {
    save(id, name, data);
    delete debounceTimers[key];  // Optional: cleanup after the save operation is done
  }, 300);  // Set a delay of 300ms (adjust as needed)
}

function save(id, name, data) {
  const changed = {
    id: id,
    name: name,
    data: data
  };

  window.Retool.modelUpdate({ 'changed': changed });
  window.Retool.triggerQuery('saveGanttChanges');
}
</script>
{
  "tasks": [
    {
      "id": "group-1",
      "name": "Task creation",
      "start": "2023-10-16T00:00:00.000Z",
      "end": "2023-10-28T15:59:59.000Z",
      "progress": 0,
      "custom_class": "bar-mygroup",
      "dependencies": ""
    },
    {
      "id": "task-1",
      "name": "step1",
      "start": "2023-10-16T00:00:00.000Z",
      "end": "2023-10-21T00:00:00.000Z",
      "progress": 0,
      "custom_class": "bar-milestone",
      "dependencies": "group-1"
    },
    {
      "id": "task-2",
      "name": "STEP2",
      "start": "2023-10-18T00:00:00.000Z",
      "end": "2023-10-24T00:00:00.000Z",
      "progress": 0,
      "custom_class": "bar-milestone",
      "dependencies": "group-1"
    },
    {
      "id": "task-5",
      "name": "Step #3",
      "start": "2023-10-21T00:00:00.000Z",
      "end": "2023-10-24T00:00:00.000Z",
      "progress": 0,
      "custom_class": "bar-milestone",
      "dependencies": "group-1"
    },
    {
      "id": "task-6",
      "name": "Four",
      "start": "2023-10-21T16:00:00.000Z",
      "end": "2023-10-27T15:59:59.000Z",
      "progress": 0,
      "custom_class": "bar-milestone",
      "dependencies": "group-1"
    },
    {
      "id": "task-7",
      "name": "55555",
      "start": "2023-10-23T16:00:00.000Z",
      "end": "2023-10-28T15:59:59.000Z",
      "progress": 0,
      "custom_class": "bar-milestone",
      "dependencies": "group-1"
    }
  ]
}

image