Skip to main content
note

The complete demo source code is available on GitHub: https://github.com/DHTMLX/gantt-migrating-from-frappe.

Migrating from Frappe Gantt to DHTMLX Gantt

Introduction

This guide will walk you through the process of migrating an existing application from Frappe Gantt to DHTMLX Gantt. We'll cover all necessary steps including database schema changes, server-side API modifications, and client-side code updates.

Prerequisites

Before starting the migration, ensure you have:

  • An existing working application using Frappe Gantt
  • Node.js (>= 20.0.0) installed
  • MySQL database with Frappe Gantt data structure
  • Basic knowledge of Express.js and JavaScript

Step 1: Database Migration

Understanding Current Schema

If you followed the Frappe Gantt demo setup, you should have one table: frappe_tasks.

The frappe_tasks table structure:

Frappe tasks table

Create DHTMLX Tables

DHTMLX Gantt uses two separate tables: one for tasks and one for dependency links. Create them in the same database:

USE frappe_dhtmlx;

CREATE TABLE IF NOT EXISTS gantt_tasks (
id VARCHAR(36) NOT NULL DEFAULT (UUID()),
text VARCHAR(255) NOT NULL,
start_date DATETIME NOT NULL,
end_date DATETIME NOT NULL,
duration INT NOT NULL,
progress FLOAT NOT NULL DEFAULT 0,
parent VARCHAR(36) NOT NULL DEFAULT '0',
PRIMARY KEY (id)
);

CREATE TABLE IF NOT EXISTS gantt_links (
id VARCHAR(36) NOT NULL DEFAULT (UUID()),
source VARCHAR(36) NOT NULL,
target VARCHAR(36) NOT NULL,
type VARCHAR(1) NOT NULL,
PRIMARY KEY (id)
);

Migrate Existing Data

Now migrate your existing Frappe data to the new DHTMLX tables.

Migrate tasks:

INSERT INTO gantt_tasks (id, text, start_date, end_date, duration, progress, parent)
SELECT
id,
name, -- name → text
start, -- start → start_date
end, -- end → end_date
GREATEST(DATEDIFF(end, start), 1), -- Duration in days (minimum 1)
progress / 100.0, -- Convert percentage (0-100) to decimal (0-1)
'0' -- No hierarchy in Frappe, all tasks are root-level
FROM frappe_tasks;

You can verify the result:

SELECT * FROM gantt_tasks;

Migrate links (dependencies)

In Frappe Gantt's data structure, dependencies are stored as strings in the dependencies column of the frappe_tasks table.

In DHTMLX Gantt, tasks and links are stored in separate tables. Each link is a row with:

  • id - the link id
  • source - the id of the task the dependency starts from
  • target - the id of the task the dependency ends at
  • type - the dependency type: "0" (FS), "1" (SS), "2" (FF), "3" (SF)

Since all Frappe dependencies are FS, the migration always sets type = "0".

Create a migrate-frappe-to-dhtmlx.js file and paste the following code into it:

import mysql from 'mysql2/promise';
import 'dotenv/config';

const dbConfig = {
host: process.env.HOST,
user: process.env.MYSQL_USER,
password: process.env.PASSWORD,
database: process.env.DATABASE,
};

async function migrateFrappeToDHtmlX() {
let connection;

try {
connection = await mysql.createConnection(dbConfig);

// Query all tasks that have dependencies
const [tasks] = await connection.execute(
'SELECT id, dependencies FROM frappe_tasks WHERE dependencies IS NOT NULL AND dependencies != ""',
);

console.log(`Found ${tasks.length} tasks with dependencies`);

if (tasks.length === 0) {
console.log('No dependencies to migrate.');
return;
}

const links = [];

for (const task of tasks) {
const targetId = task.id;
const dependencies = task.dependencies;

// Split comma-separated dependency IDs
const depIds = dependencies
.split(',')
.map((dep) => dep.trim())
.filter((dep) => dep);

console.log(`\nTask ${targetId} depends on: ${depIds.join(', ')}`);

// Each dependency becomes a Finish-to-Start link (type "0")
for (const sourceId of depIds) {
links.push({
source: sourceId,
target: targetId,
type: '0',
});
}
}

if (links.length > 0) {
console.log(`\nInserting ${links.length} links into gantt_links...`);

await connection.beginTransaction();

try {
await connection.execute('DELETE FROM gantt_links');
console.log('Cleared existing links from gantt_links table');

for (const link of links) {
await connection.execute('INSERT INTO gantt_links (source, target, type) VALUES (?, ?, ?)', [
link.source,
link.target,
link.type,
]);
}

await connection.commit();
console.log('Links inserted successfully!');
} catch (error) {
await connection.rollback();
throw error;
}
}

const [insertedLinks] = await connection.execute('SELECT * FROM gantt_links');
console.log(`Total links in gantt_links: ${insertedLinks.length}`);

console.log('\nMigration completed successfully!');
} catch (error) {
console.error('Migration failed:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\nDatabase connection closed.');
}
}
}

migrateFrappeToDHtmlX();

Then add a script to your package.json:

{
"scripts": {
"migrate": "node migrate-frappe-to-dhtmlx.js"
}
}

Run the migration:

npm run migrate

You can verify that the links were migrated correctly:

SELECT * FROM gantt_links;

You should see one row per dependency, with correct source and target IDs.

Mapping Frappe Task Fields to DHTMLX Gantt

Frappe FieldDHTMLX FieldNotes
ididTask id
nametextTask name
startstart_dateThe start date of the task
endend_dateThe end date of the task
(not stored)durationThe task duration. In DHTMLX Gantt, if not specified, Gantt will calculate it based on the start_date and end_date properties
progressprogressFrappe: integer 0–100; DHTMLX: decimal 0.0–1.0
(not supported)parentFrappe has no hierarchy. In DHTMLX Gantt you can specify the parent task
dependencies(links table)Frappe stores as strings; DHTMLX uses a separate gantt_links table

Step 2: Backend Migration (server.js)

Remove Frappe-Specific Endpoints and Helper

In the Frappe server, data loading and CRUD for tasks go through /data/tasks. Delete or replace all of the following:

  • function formatTaskForClient(dbTask) - the Frappe-specific response formatter
  • app.get('/data/tasks', ...) - returns a plain array of task objects
  • app.post('/data/tasks', ...) - creates a task; response returns the full task object
  • app.put('/data/tasks/:id', ...) - updates a task; response returns the updated task object
  • app.delete('/data/tasks/:id', ...) - deletes a task; returns HTTP 204 with no body

Install DHTMLX Gantt Package

Install DHTMLX Gantt following the installation guide.

For this tutorial, we will use the trial version of DHTMLX Gantt:

npm install @dhx/trial-gantt

Add Data Loading Endpoint

DHTMLX expects both tasks and links to be returned in a single GET /data response as { tasks: [], links: [] }.

Replace the Frappe GET /data/tasks endpoint with:

import dateFormat from 'date-format-lite';

// GET /data - Load tasks and links
app.get('/data', async (req, res) => {
try {
const [tasks] = await pool.query('SELECT * FROM gantt_tasks ORDER BY start_date');
const [links] = await pool.query('SELECT * FROM gantt_links');

tasks.forEach((task) => {
if (task.start_date) {
task.start_date = task.start_date.format('YYYY-MM-DD hh:mm:ss');
}
if (task.end_date) {
task.end_date = task.end_date.format('YYYY-MM-DD hh:mm:ss');
}
});

res.json({ tasks, links });
} catch (error) {
console.error('Error loading data:', error);
res.status(500).json({ error: 'Failed to load data' });
}
});

DHTMLX Gantt's DataProcessor uses RESTful endpoints to synchronize data with the server. Each operation (create, update, delete) is sent as a separate HTTP request. Learn more about Server-side integration.

Replace the Frappe task endpoints (POST /data/tasks, PUT /data/tasks/:id, DELETE /data/tasks/:id) with:

import { randomUUID } from 'crypto';

// POST /data/task — Create a new task
app.post('/data/task', async (req, res) => {
try {
const task = getTask(req.body);
const { text, start_date, end_date, duration, progress, parent } = task;
const id = randomUUID();

await pool.query(
'INSERT INTO gantt_tasks (id, text, start_date, end_date, duration, progress, parent) VALUES (?, ?, ?, ?, ?, ?, ?)',
[id, text, start_date, end_date, duration, progress, parent],
);
sendResponse(res, 'inserted', id);
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// PUT /data/task/:id — Update an existing task
app.put('/data/task/:id', async (req, res) => {
try {
const taskId = req.params.id;
const task = getTask(req.body);
const { text, start_date, end_date, duration, progress, parent } = task;

await pool.query(
'UPDATE gantt_tasks SET text = ?, start_date = ?, end_date = ?, duration = ?, progress = ?, parent = ? WHERE id = ?',
[text, start_date, end_date, duration, progress, parent, taskId],
);
sendResponse(res, 'updated');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// DELETE /data/task/:id — Delete a task
app.delete('/data/task/:id', async (req, res) => {
try {
const taskId = req.params.id;
await pool.query('DELETE FROM gantt_tasks WHERE id = ?', [taskId]);
sendResponse(res, 'deleted');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

Add handlers for link (dependency) operations:

// POST /data/link — Create a new link
app.post('/data/link', async (req, res) => {
try {
const link = getLink(req.body);
const id = randomUUID();

await pool.query('INSERT INTO gantt_links (id, source, target, type) VALUES (?, ?, ?, ?)', [
id,
link.source,
link.target,
link.type,
]);
sendResponse(res, 'inserted', id);
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// PUT /data/link/:id — Update an existing link
app.put('/data/link/:id', async (req, res) => {
try {
const linkId = req.params.id;
const link = getLink(req.body);

await pool.query('UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?', [
link.source,
link.target,
link.type,
linkId,
]);
sendResponse(res, 'updated');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// DELETE /data/link/:id — Delete a link
app.delete('/data/link/:id', async (req, res) => {
try {
const linkId = req.params.id;
await pool.query('DELETE FROM gantt_links WHERE id = ?', [linkId]);
sendResponse(res, 'deleted');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

Add Helper Functions

Replace the Frappe formatTaskForClient function with DHTMLX-compatible helpers:

// Parse task data from request body
function getTask(data) {
return {
text: data.text,
start_date: data.start_date,
end_date: data.end_date,
duration: data.duration || 1,
progress: parseFloat(data.progress) || 0,
parent: data.parent || 0,
};
}

// Parse link data from request body
function getLink(data) {
return {
source: data.source,
target: data.target,
type: data.type,
};
}

// Send DataProcessor-compatible response
function sendResponse(res, action, tid = null, error = null) {
if (error) {
console.error('Error:', error);
return res.status(500).json({ action: 'error', message: error.message });
}
const result = { action };
if (tid !== null) result.tid = tid;
res.json(result);
}

Note: The response format is different from Frappe. Frappe endpoints returned the full task object (or HTTP 204 for deletes). DHTMLX's DataProcessor expects a JSON object with an action field (e.g., { action: "inserted", tid: 5 }, { action: "updated" }, { action: "deleted" }). Learn more the Request and Responses details.

Step 3: Frontend Migration

Install the DHTMLX Gantt package

For this tutorial, we will use the trial version of DHTMLX Gantt:

npm install @dhx/trial-gantt

Update vite.config.js

In the Frappe demo, the Vite proxy was scoped to /data/tasks:

proxy: {
'/data/tasks': {
target: 'http://localhost:1337',
changeOrigin: true,
},
},

Update it to proxy all /data requests (which now cover tasks, task CRUD, and link CRUD):

proxy: {
'/data': {
target: 'http://localhost:1337',
changeOrigin: true,
},
},

Update index.html

In the Frappe demo, frappe-gantt is loaded from a CDN. There is also a complex custom UI with modals, checkboxes, and control buttons - all of which are no longer needed because DHTMLX Gantt provides a built-in lightbox for editing tasks.

Replace the entire index.html content:

Remove:

<script src="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css" />

Also remove the entire custom UI markup inside <body> including:

  • The .controls div with #add-task, #refresh, and #delete-task buttons
  • The .delete-section div with task checkboxes
  • The #add-task-modal overlay
  • The <div id="gantt"> container

Replace with a minimal structure:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charSet="UTF-8" />
<link rel="stylesheet" href="/src/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DHTMLX Gantt Demo</title>
</head>
<body>
<div id="gantt_here"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

Update src/style.css

The Frappe demo's style.css contains custom styles for the control panel, buttons, modals, and checkboxes. Since all that custom UI is removed, replace the file with DHTMLX-specific styles:

html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}

#gantt_here {
width: 100%;
height: 100%;
}

.weekend {
background: var(--dhx-gantt-base-colors-background-alt);
}

The .weekend class is used by DHTMLX Gantt templates to highlight weekend columns (see src/main.js).

Replace src/main.js

Remove all Frappe related code and replace src/main.js with:

import '@dhx/trial-gantt/codebase/dhtmlxgantt.css';
import gantt from '@dhx/trial-gantt';

gantt.plugins({
auto_scheduling: true,
tooltip: true,
});

gantt.config.auto_scheduling = {
enabled: true,
};

const hourToStr = gantt.date.date_to_str('%H:%i');
const hourRangeFormat = function (step) {
return function (date) {
const intervalEnd = new Date(gantt.date.add(date, step, 'hour') - 1);
return hourToStr(date) + ' - ' + hourToStr(intervalEnd);
};
};

const zoomConfig = {
minColumnWidth: 80,
maxColumnWidth: 150,
levels: [
[
{ unit: 'month', format: '%M %Y', step: 1 },
{
unit: 'week',
step: 1,
format: function (date) {
const dateToStr = gantt.date.date_to_str('%d %M');
const endDate = gantt.date.add(date, 7 - date.getDay(), 'day');
const weekNum = gantt.date.date_to_str('%W')(date);
return 'Week #' + weekNum + ', ' + dateToStr(date) + ' - ' + dateToStr(endDate);
},
},
],
[
{ unit: 'month', format: '%M %Y', step: 1 },
{ unit: 'day', format: '%d %M', step: 1 },
],
[
{ unit: 'day', format: '%d %M', step: 1 },
{ unit: 'hour', format: hourRangeFormat(12), step: 12 },
],
[
{ unit: 'day', format: '%d %M', step: 1 },
{ unit: 'hour', format: hourRangeFormat(8), step: 8 },
],
[
{ unit: 'day', format: '%d %M', step: 1 },
{ unit: 'hour', format: '%H:%i', step: 1 },
],
],
useKey: 'ctrlKey',
trigger: 'wheel',
element: function () {
return gantt.$root.querySelector('.gantt_task');
},
};

gantt.ext.zoom.init(zoomConfig);

gantt.templates.scale_cell_class = function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'weekend';
}
};
gantt.templates.timeline_cell_class = function (item, date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'weekend';
}
};

gantt.config.date_format = '%Y-%m-%d %H:%i:%s';
gantt.config.scale_height = 50;
gantt.config.open_tree_initially = true;

gantt.init('gantt_here');
gantt.load('/data');

const dp = gantt.createDataProcessor({
url: '/data',
mode: 'REST',
});

The DataProcessor will automatically:

  • Send POST to /data/task when creating a task
  • Send PUT to /data/task/:id when updating a task
  • Send DELETE to /data/task/:id when deleting a task
  • Send POST to /data/link when creating a dependency link
  • Send PUT to /data/link/:id when updating a link
  • Send DELETE to /data/link/:id when deleting a link

Step 4: Testing the Migration

Running the Application

For development mode, you need to run two processes.

Terminal 1 — Backend (Express):

npm run server

This starts the API server on http://localhost:1337 (or your configured port).

Terminal 2 — Frontend (Vite):

npm run dev

This starts the Vite dev server on http://localhost:5173. Open your browser and navigate to http://localhost:5173. Vite will proxy /data requests to the Express backend automatically.

You should see the DHTMLX Gantt chart with your migrated data loaded from the database.

DHTMLX Gantt Chart

Next Steps

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.