diff --git a/docs/guides/guides.md b/docs/guides/guides.md index 39f0ad75..3833170a 100644 --- a/docs/guides/guides.md +++ b/docs/guides/guides.md @@ -165,5 +165,15 @@ Describes the elements of the Gantt Chart interface from the point of view of th - ### [Interface of Gantt Chart](guides/overview.md) +## Migrating from Other Gantt Libraries + +Step-by-step tutorials for moving an existing app from Bryntum, Syncfusion, DevExpress, or Frappe Gantt to DHTMLX Gantt, with companion demo repositories on GitHub. + +- ### [Migrating to DHTMLX Gantt](/migrating/) +- ### [From Bryntum](/migrating/from-bryntum) +- ### [From Syncfusion](/migrating/from-syncfusion) +- ### [From DevExpress](/migrating/from-devexpress) +- ### [From Frappe](/migrating/from-frappe) + \ No newline at end of file diff --git a/docs/migrating/from-bryntum.md b/docs/migrating/from-bryntum.md new file mode 100644 index 00000000..ae956188 --- /dev/null +++ b/docs/migrating/from-bryntum.md @@ -0,0 +1,801 @@ +--- +title: "Migrating from Bryntum to DHTMLX Gantt" +sidebar_label: "From Bryntum" +--- + +:::note +The complete demo source code is available on GitHub: [https://github.com/DHTMLX/gantt-migrating-from-bryntum](https://github.com/DHTMLX/gantt-migrating-from-bryntum). +::: + +# Migrating from Bryntum Gantt to DHTMLX Gantt + +## Introduction + +[Bryntum Gantt](https://bryntum.com/products/gantt/) is a JavaScript Gantt chart component for project management tools. + +This guide will walk you through the process of migrating an existing application from Bryntum 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 Bryntum Gantt +- Node.js (>= 20.0.0) installed +- MySQL database with Bryntum data structure +- Basic knowledge of Express.js and JavaScript + + +## Step 1: Database Migration + +### Understanding Current Schema + +If you followed the Bryntum demo setup, you should have two tables: `tasks` and `dependencies`. + +The `tasks` table structure: +![Bryntum tasks table](/img/migrating/bryntum/bryntum-tasks-mysql.png) + +The `dependencies` table structure: +![Bryntum dependencies table](/img/migrating/bryntum/bryntum-deps-mysql.png) + +### Create DHTMLX Tables + +DHTMLX Gantt uses a simpler database structure. Create two new tables compatible with DHTMLX Gantt: + +```sql +CREATE TABLE `gantt_tasks` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `text` varchar(255) NOT NULL, + `start_date` datetime NOT NULL, + `end_date` datetime NOT NULL, + `duration` int(11) NOT NULL, + `progress` float NOT NULL, + `parent` int(11) NOT NULL, + `constraint_type` varchar(20) DEFAULT 'asap', + `constraint_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `gantt_links` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `source` int(11) NOT NULL, + `target` int(11) NOT NULL, + `type` varchar(1) NOT NULL, + PRIMARY KEY (`id`) +); +``` + +**Note:** +- We've added `constraint_type` and `constraint_date` fields to support auto-scheduling with constraints, which is a feature available in both Bryntum and DHTMLX Gantt. +- We've added `end_date` field to preserve the exact end date calculated by the Gantt. Without storing `end_date`, it would be recalculated based on `start_date` and `duration`, which can vary depending on enabled features (auto-scheduling, work time, constraints). Storing `end_date` prevents loss of this fundamental information. + +### Migrate Existing Data + +Now migrate your existing Bryntum data to the new DHTMLX tables. + +**Migrate tasks:** +```sql +INSERT INTO gantt_tasks ( + `id`, + `text`, + `start_date`, + `end_date`, + `duration`, + `progress`, + `parent`, + `constraint_type`, + `constraint_date` +) +SELECT + `id`, + `name`, -- 'name' --> 'text' + `startDate`, + DATE_ADD(`startDate`, INTERVAL `duration` DAY), -- Calculate end_date from start_date + duration + `duration`, + `percentDone`, -- 'percentDone' --> 'progress' + IFNULL(`parentId`, 0), -- 'parentId' --> 'parent' (0 for root tasks) + CASE `constraintType` -- Map constraint types + WHEN 'assoonaspossible' THEN 'asap' + WHEN 'aslateaspossible' THEN 'alap' + WHEN 'startnoearlierthan' THEN 'snet' + WHEN 'startnolaterthan' THEN 'snlt' + WHEN 'finishnoearlierthan' THEN 'fnet' + WHEN 'finishnolaterthan' THEN 'fnlt' + WHEN 'muststarton' THEN 'mso' + WHEN 'mustfinishon' THEN 'mfo' + ELSE 'asap' + END, + `constraintDate` +FROM tasks; +``` + +**Migrate links (dependencies):** +```sql +INSERT INTO gantt_links (`id`, `source`, `target`, `type`) +SELECT + `id`, + `fromEvent`, -- 'fromEvent' --> 'source' + `toEvent`, -- 'toEvent' --> 'target' + CASE `type` -- Convert Bryntum link types to DHTMLX format + WHEN 0 THEN '1' -- Start-to-Start + WHEN 1 THEN '3' -- Start-to-Finish + WHEN 2 THEN '0' -- Finish-to-Start (most common) + WHEN 3 THEN '2' -- Finish-to-Finish + ELSE '0' -- Default to Finish-to-Start + END +FROM dependencies; +``` + +You can verify that the data was migrated correctly by running the following commands: + +```sql +SELECT * FROM gantt_tasks; +SELECT * FROM gantt_links; +``` + +You should see all your tasks and links properly transferred with the correct field mappings. + +### Mapping Bryntum Task Fields to DHTMLX Gantt + +Bryntum Gantt's [TaskModel](https://bryntum.com/products/gantt/docs/api/Gantt/model/TaskModel) contains a number of fields that are either implemented differently in DHTMLX Gantt or require special handling during migration. The following table explains how to map the most common Bryntum task fields to DHTMLX Gantt: + +| Bryntum Field | Description | Recommended Approach | +|--------------|-------------|---------------------| +| `effort` / `effortUnit` | Amount of work required to complete a task (e.g. 16h), used for effort-driven and resource-based scheduling | DHTMLX Gantt does not support effort-driven scheduling at the task level. As an alternative, you can use the [Resource Management](guides/resource-management.md) module to assign resources and visualize workload. Resource assignments can represent effort (e.g. hours per day), but task duration is not recalculated automatically and must be managed manually or via custom logic. | +| `durationUnit` | Unit used to interpret the task duration (hours, days, weeks, etc.) | DHTMLX Gantt uses a global duration unit configured via `gantt.config.duration_unit`. During migration, it's recommended to normalize all durations to a single unit. If you want to have different duration units for different tasks, i.e. to show durations of some tasks in hours and some tasks in "days", you can use the [formatter module](https://docs.dhtmlx.com/gantt/guides/working-time/#taskdurationindecimalformat). | +| `schedulingMode` | Defines how task scheduling behaves (Normal, FixedDuration, FixedEffort, etc.) | No direct equivalent. DHTMLX Gantt does not support per-task scheduling modes. You can store this value as a custom field and, if required, enforce custom behavior using Gantt events (e.g., [onBeforeTaskUpdate](api/event/onbeforetaskupdate.md)). | +| `note` | Free-text notes or description attached to a task | Can be migrated directly as a custom text field (e.g. `note` or `description`) and shown in the lightbox, tooltip, or a custom grid column. | +| `manuallyScheduled` | Indicates whether a task is excluded from automatic scheduling | The `task.auto_scheduling` property of the task can be used which allows individual tasks to be excluded from auto scheduling while keeping it enabled globally. See [Disabling auto scheduling for specific tasks](guides/auto-scheduling.md#disabling-auto-scheduling-for-specific-tasks). | +| `calendar` | The calendar, assigned to the task | DHTMLX Gantt supports multiple working calendars. A calendar can be assigned to a task via the `calendar_id` property (or a custom property defined by `gantt.config.calendar_property`). See [Assigning Calendar to Task](guides/working-time.md#assigningcalendartotask). | +| `deadline` | A target date that the task should not exceed | Fully supported via the `task.deadline` property. When specified, DHTMLX Gantt displays a visual deadline indicator on the timeline. The value uses the same date format as `start_date`. | +--- + +## Step 2: Backend Migration (server.js) + +### Remove Bryntum-Specific Code + +First, remove the Bryntum package serving middleware from your `server.js`: + +```js +// DELETE this line: +app.use(express.static(path.join(__dirname, '/node_modules/@bryntum/gantt'))); +``` + +**Important:** With Vite which will be used in this demo, you no longer need to serve `node_modules` directly. Remove any middleware that exposes the entire `node_modules` directory: + +```js +// DELETE this line if present: +app.use('/node_modules', express.static(path.join(__dirname, 'node_modules'))); +``` + +### Remove Bryntum Endpoints + +Delete the following Bryntum-specific endpoints and helper functions: +- `app.get('/load', ...)` - Bryntum data loading endpoint +- `app.post('/sync', ...)` - Bryntum sync endpoint +- `applyTableChanges()` function +- `createOperation()` function +- `updateOperation()` function +- `deleteOperation()` function + +### Install DHTMLX Gantt Package and Vite + +Remove Bryntum dependency. If you were using Bryntum via npm, uninstall it: +```bash +npm uninstall @bryntum/gantt +``` + +Install DHTMLX Gantt following the [installation guide](guides/installation.md). + +For this tutorial, we will use the trial version of DHTMLX Gantt: + +```bash +npm install @dhx/trial-gantt +``` + +Let's also install Vite as a build tool: + +```bash +npm install --save-dev vite +``` + +### Add Data Loading Endpoint + +Add the GET endpoint to load data in DHTMLX format: + +```js +import dateFormat from 'date-format-lite'; + +// GET /data - Load tasks and links +app.get('/data', async (req, res) => { + try { + const [[tasks], [links]] = await Promise.all([ + db.query('SELECT * FROM gantt_tasks'), + db.query('SELECT * FROM gantt_links'), + ]); + + // Format dates for DHTMLX Gantt + for (let i = 0; i < tasks.length; i++) { + tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss"); + tasks[i].end_date = tasks[i].end_date.format("YYYY-MM-DD hh:mm:ss"); + // Format constraint_date if it exists + if (tasks[i].constraint_date) { + tasks[i].constraint_date = tasks[i].constraint_date.format("YYYY-MM-DD hh:mm:ss"); + } + } + + res.json({ + tasks, + links + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error.message, + }); + } +}); +``` + +**Note:** The response format is different from Bryntum. DHTMLX expects `{ tasks: [], links: [] }` instead of Bryntum's nested structure. + +### Add CRUD Endpoints for Tasks and Links + +DHTMLX Gantt DataProcessor uses RESTful endpoints. Add handlers for task operations: + +```js +// Create a new task +app.post("/data/task", async (req, res) => { + const task = getTask(req.body); + const { text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date } = task; + + try { + const [result] = await db.query( + "INSERT INTO gantt_tasks(text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date) VALUES (?,?,?,?,?,?,?,?)", + [text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date] + ); + sendResponse(res, "inserted", result.insertId); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); + +// Update an existing task +app.put("/data/task/:id", async (req, res) => { + const sid = req.params.id; + const task = getTask(req.body); + const { text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date } = task; + + try { + await db.query( + "UPDATE gantt_tasks SET text = ?, start_date = ?, end_date = ?, duration = ?, progress = ?, parent = ?, constraint_type = ?, constraint_date = ? WHERE id = ?", + [text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date, sid] + ); + sendResponse(res, "updated"); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); + +// Delete a task +app.delete("/data/task/:id", async (req, res) => { + const sid = req.params.id; + + try { + await db.query("DELETE FROM gantt_tasks WHERE id = ?", [sid]); + sendResponse(res, "deleted"); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); +``` + +Add handlers for link (dependency) operations: + +```js +// Create a new link +app.post("/data/link", async (req, res) => { + const link = getLink(req.body); + const { source, target, type } = link; + + try { + const [result] = await db.query( + "INSERT INTO gantt_links(source, target, type) VALUES (?,?,?)", + [source, target, type] + ); + sendResponse(res, "inserted", result.insertId); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); + +// Update an existing link +app.put("/data/link/:id", async (req, res) => { + const sid = req.params.id; + const link = getLink(req.body); + const { source, target, type } = link; + + try { + await db.query( + "UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?", + [source, target, type, sid] + ); + sendResponse(res, "updated"); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); + +// Delete a link +app.delete("/data/link/:id", async (req, res) => { + const sid = req.params.id; + + try { + await db.query("DELETE FROM gantt_links WHERE id = ?", [sid]); + sendResponse(res, "deleted"); + } catch (error) { + sendResponse(res, "error", null, error); + } +}); +``` + +### Add Helper Functions + +Also, let's add utility functions to process data and send responses: + +```js +function getTask(data) { + return { + text: data.text, + start_date: data.start_date.date("YYYY-MM-DD"), + end_date: data.end_date.date("YYYY-MM-DD"), + duration: data.duration, + progress: data.progress || 0, + parent: data.parent, + constraint_type: data.constraint_type || 'asap', + constraint_date: data.constraint_date || null + }; +} + +function getLink(data) { + return { + source: data.source, + target: data.target, + type: data.type + }; +} + +function sendResponse(res, action, tid, error) { + if (action === "error") { + console.log(error); + } + + const result = { action: action }; + + if (tid !== undefined && tid !== null) { + result.tid = tid; + } + + res.send(result); +} +``` + +--- + +## Step 3: Frontend Migration with Vite + +### Set Up Vite Configuration + +Create a `vite.config.js` file in the root of your project: + +```javascript +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + root: '.', + + server: { + port: 5173, + proxy: { + // Proxy API requests to Express backend + '/data': { + target: 'http://localhost:1337', + changeOrigin: true, + } + } + }, + + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: true, + }, + + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + } +}); +``` + +### Restructure Project Files + +Organize your project with this structure: + +``` +dhtmlx-demo/ +├── index.html # Move from public/ to root +├── src/ +│ └── main.js # Create this file for app logic +├── dist/ # Generated by Vite build +├── server.js # Backend +├── vite.config.js # Vite configuration +└── package.json +``` + +### Update index.html + +Move `index.html` to the project root and update it. With Vite, you don't need to manually include CSS and JS files - Vite will bundle them automatically. + +```html + + + + + + DHTMLX Gantt Demo + + + + +
+ + + +``` + +**Note:** The container ID changed to `gantt_here`, which is DHTMLX Gantt's conventional container ID. + +### Create src/main.js + +Create a new `src/main.js` file with your DHTMLX Gantt initialization code: + +Remove Bryntum code: +```js +// DELETE all Bryntum imports and initialization +import { Gantt, ProjectModel } from './gantt.module.js'; + +const project = new ProjectModel({ + taskStore: { transformFlatData: true }, + loadUrl: '/load', + syncUrl: '/sync', + autoLoad: true, + autoSync: true, +}); + +const gantt = new Gantt({ + appendTo: document.body, + project, + columns: [...] +}); +``` + +Add DHTMLX code in src/main.js: +```js +// Import DHTMLX Gantt CSS and library +import '@dhx/trial-gantt/codebase/dhtmlxgantt.css'; +import gantt from '@dhx/trial-gantt'; + +// Enable plugins +gantt.plugins({ + auto_scheduling: true, + marker: true, + tooltip: true +}); + +// Configure auto-scheduling with constraints +gantt.config.auto_scheduling = { + enabled: true, + show_constraints: true, + apply_constraints: true, + project_constraint: true, +}; + +// Project settings +gantt.config.project_start = new Date(2026, 10, 5); + +// Enable work time +gantt.config.work_time = true; + +// Enable additional features +gantt.config.drag_project = true; // Drag projects +gantt.config.order_branch = true; // Vertically reorder tasks within the same tree level + +// Date format +gantt.config.date_format = "%Y-%m-%d %H:%i:%s"; +gantt.config.open_tree_initially = true; + +// Configure columns to display constraint information +gantt.config.columns = [ + { name: "text", tree: true, width: '*', resize: true, width: 150 }, + { name: "start_date", align: "center", resize: true, width: 150 }, + { name: "duration", align: "center", width: 80, resize: true, }, + { + name: "constraint_type", align: "center", width: 100, template: function (task) { + return gantt.locale.labels[gantt.getConstraintType(task)]; + }, resize: true, + }, + { + name: "constraint_date", align: "center", width: 120, template: function (task) { + const constraintTypes = gantt.config.constraint_types; + + if (task.constraint_date && task.constraint_type != constraintTypes.ASAP && task.constraint_type != constraintTypes.ALAP) { + return task.constraint_date; + } + return ""; + }, resize: true, + }, + { name: "add", width: 44 } +]; + +// Configure lightbox sections +gantt.config.lightbox.sections = [ + { name:"description", height:38, map_to:"text", type:"textarea", focus:true}, + { name:"constraint", type:"constraint" }, + { name:"time", type:"duration", map_to:"auto" } +]; + +// Configure mouse wheel zoom +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(6),step: 6} + ], + [ + { unit: "day", format: "%d %M", step: 1 }, + { unit: "hour", format: "%H:%i", step: 1} + ] + ], + startDate: new Date(2026, 10, 5), + endDate: new Date(2026, 10, 20), + useKey: "ctrlKey", + trigger: "wheel", + element: function(){ + return gantt.$root.querySelector(".gantt_task"); + } +} + +gantt.ext.zoom.init(zoomConfig); + +// Add marker for project start +gantt.addMarker({ + start_date: gantt.config.project_start, + text: "project start" +}); + +// Highlight weekends in the timeline +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"; + } +}; + +// Initialize Gantt +gantt.init("gantt_here"); + +// Load data from server +gantt.load("/data"); + +const dp = gantt.createDataProcessor({ + url: '/data', + mode: 'REST' +}); +``` + +The DataProcessor will automatically: +- Send POST requests to `/data/task` when creating tasks +- Send PUT requests to `/data/task/:id` when updating tasks +- Send DELETE requests to `/data/task/:id` when deleting tasks +- Handle links similarly with `/data/link` endpoints + +### Enable Bryntum Default Features + +**Important Note:** To use some features enabled in Bryntum Gantt by default, you need to enable them in DHTMLX Gantt explicitly. The configuration above includes several features that are standard in Bryntum: + +#### Auto-Scheduling with Constraints +In DHTMLX Gantt, you need to: +1. Enable the `auto_scheduling` plugin +2. Configure `gantt.config.auto_scheduling` settings + +[Time constraints for tasks](guides/auto-scheduling.md#timeconstraintsfortasks) +[Auto Scheduling](guides/auto-scheduling.md) + +#### Work Time and Weekend Highlighting +To highlight non-working days in the timeline: +- Enable `work_time` configuration +- Use `scale_cell_class` and `timeline_cell_class` templates to highlight weekends + +[Work Time Calculation](guides/working-time.md) + +#### Tooltips +Enable the tooltip plugin to show task information on hover. +```js +gantt.plugins({ + tooltip: true +}); +``` +[Tooltips for Gantt Elements](guides/tooltips.md) + +#### Mouse Wheel Zoom +Configure `gantt.ext.zoom` to enable zooming with the mouse wheel, allowing users to switch between day, week, month, and other views. + +[Zoom Extension](guides/zoom.md) + +#### Drag & Drop Features +- `drag_project`: Enables drag and drop of items of the project type +- `order_branch`: Allows vertically reorder tasks within the same tree level + +**Note about Task Ordering:** When `order_branch` is enabled, users can reorder tasks in the UI. However, these changes are **not automatically saved to the database**. To persist task order, you need to implement additional server-side logic. Check [this guide](integrations/node/howtostart-nodejs.md#enable-tasks-reordering-on-the-client) for detailed implementation instructions. + +#### Project Markers +Add visual markers to highlight important dates (like project start) on the timeline. + +[Adding Vertical Markers](guides/markers.md) + +### Add Weekend Styling + +Add CSS styles to your `index.html`: + +```html + +``` + +### Update package.json Scripts + +Update your `package.json` scripts to use Vite: + +```json +{ + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "server": "nodemon server.js" + } +} +``` + +--- + +## Step 4: Testing the Migration + +### Development Mode + +For development, you need to run two processes: + +**Terminal 1 - Backend (Express):** +```bash +npm run server +``` +This starts the API server on `http://localhost:1337` + +**Terminal 2 - Frontend (Vite):** +```bash +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 API requests to the Express backend automatically. + +You should see the DHTMLX Gantt chart with your data loaded from the database: + +![Gantt with data loaded](/img/migrating/bryntum/gantt-data-loaded.png) + +### Production Mode + +For production, first build the frontend: + +```bash +npm run build +``` + +This creates an optimized bundle in the `dist/` folder. Then update your `server.js` to serve the built files: + +```javascript +import path from 'path'; + +// In server.js, add this for production +const __dirname = import.meta.dirname; + +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, 'dist'))); +} +``` + +Now you can run just the backend: +```bash +npm run server +``` + +And access the application at `http://localhost:1337` + +## Next Steps + +- Explore [DHTMLX Gantt documentation](https://docs.dhtmlx.com/gantt/) for advanced features +- Review the [API reference](https://docs.dhtmlx.com/gantt/api__refs__gantt.html) for customization options +- Check out [DHTMLX Gantt samples](https://docs.dhtmlx.com/gantt/samples/) for implementation examples \ No newline at end of file diff --git a/docs/migrating/from-devexpress.md b/docs/migrating/from-devexpress.md new file mode 100644 index 00000000..32dce65e --- /dev/null +++ b/docs/migrating/from-devexpress.md @@ -0,0 +1,558 @@ +--- +title: "Migrating from DevExpress to DHTMLX Gantt" +sidebar_label: "From DevExpress" +--- + +:::note +The complete demo source code is available on GitHub: [https://github.com/DHTMLX/gantt-migrating-from-devexpress](https://github.com/DHTMLX/gantt-migrating-from-devexpress). +::: + +# Migrating from DevExpress Gantt to DHTMLX Gantt + +## Introduction + +This guide will walk you through the process of migrating an existing application from [DevExpress Gantt](https://js.devexpress.com/React/Documentation/Guide/UI_Components/Gantt/Overview/) to [DHTMLX Gantt](https://dhtmlx.com/docs/products/dhtmlxGantt/). 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 DevExpress Gantt +- Node.js (>= 20.0.0) installed +- MySQL database with DevExpress data structure +- Basic knowledge of Express.js, React, and TypeScript + +## Step 1: Database Migration + +### Understanding DevExpress Schema + +If you followed the DevExpress demo setup, you should +have two tables: `devexpress_tasks` and `devexpress_dependencies`. + +The `devexpress_tasks` table structure: + +![DevExpress tasks table](/img/migrating/devexpress/devexpress-tasks-table.png) + +The `devexpress_dependencies` table structure: + +![DevExpress links table](/img/migrating/devexpress/devexpress-links-table.png) + +This two-table structure is already similar to DHTMLX's approach, making the migration straightforward. + +### Create DHTMLX Tables + +Create two new tables compatible with DHTMLX Gantt: + +```sql +CREATE TABLE IF NOT EXISTS gantt_tasks ( + id INT(11) NOT NULL AUTO_INCREMENT, + text VARCHAR(255) NOT NULL, + start_date DATETIME NOT NULL, + end_date DATETIME NOT NULL, + progress FLOAT NOT NULL DEFAULT 0, + parent INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS gantt_links ( + id INT(11) NOT NULL AUTO_INCREMENT, + source INT(11) NOT NULL, + target INT(11) NOT NULL, + type VARCHAR(1) NOT NULL, + PRIMARY KEY (id) +); +``` + +**Note:** DHTMLX Gantt will automatically calculate `duration` based on `start_date` and `end_date`. + +### Migrate Existing Data + +Now migrate your existing DevExpress data to the new DHTMLX tables. + +**Migrate tasks:** + +```sql +INSERT INTO gantt_tasks (id, text, start_date, end_date, progress, parent) +SELECT + id, + title, + start, + end, + progress / 100, + COALESCE(parentId, 0) +FROM devexpress_tasks; +``` + +**Migrate links (dependencies):** + +DevExpress already stores dependencies in a structured format in the `devexpress_dependencies` table, which makes migration straightforward: + +```sql +INSERT INTO gantt_links (id, source, target, type) +SELECT + id, + predecessorId, -- predecessorId → source + successorId, -- successorId → target + CASE type + WHEN 0 THEN '0' -- Finish-to-Start + WHEN 1 THEN '1' -- Start-to-Start + WHEN 2 THEN '2' -- Finish-to-Finish + WHEN 3 THEN '3' -- Start-to-Finish + ELSE '0' + END +FROM devexpress_dependencies; +``` + +You can verify that the data was migrated correctly by running the following commands: + +```sql +SELECT * FROM gantt_tasks; +SELECT * FROM gantt_links; +``` + +You should see all your tasks and links properly transferred with the correct field mappings. + +### Mapping DevExpress Task Fields to DHTMLX Gantt + +| DevExpress Field | DHTMLX Field | Notes | +| ---------------- | ------------ | -------------------------------------------------------------------------------- | +| `id` | `id` | Task ID | +| `title` | `text` | Task name | +| `start` | `start_date` | Task start date and time | +| `end` | `end_date` | Task end date and time | +| `progress` | `progress` | DevExpress: 0-100 (integer), DHTMLX: 0-1 (float). Divide by 100 during migration | +| `parentId` | `parent` | Parent task ID. NULL values → 0 for root tasks | + +More about task properties: [Task Properties](https://docs.dhtmlx.com/gantt/guides/task-properties/). + +### Mapping DevExpress Dependency Fields to DHTMLX Links + +| DevExpress Field | DHTMLX Field | Notes | +| ---------------- | ------------ | ---------------------------------------------------------------------------------------- | +| `id` | `id` | Link ID | +| `predecessorId` | `source` | ID of the task that the dependency starts from | +| `successorId` | `target` | ID of the task that the dependency points to | +| `type` | `type` | Dependency type. DevExpress uses numbers (0-3), DHTMLX uses strings ("0"-"3") by default | + +More about link properties: [Link Properties](https://docs.dhtmlx.com/gantt/guides/link-properties/). + +## Step 2: Backend Migration (server.js) + +### Remove DevExpress Endpoints + +Delete the following DevExpress-specific endpoints from your `server.js`: + +- `app.get('/api/tasks', ...)` - DevExpress tasks loading endpoint +- `app.post('/api/tasks', ...)` - Create task endpoint +- `app.put('/api/tasks/:id', ...)` - Update task endpoint +- `app.delete('/api/tasks/:id', ...)` - Delete task endpoint +- `app.get('/api/dependencies', ...)` - DevExpress dependencies loading endpoint +- `app.post('/api/dependencies', ...)` - Create dependency endpoint +- `app.put('/api/dependencies/:id', ...)` - Update dependency endpoint +- `app.delete('/api/dependencies/:id', ...)` - Delete dependency endpoint + +Also remove the CustomStore-related response format handling. + +### Install DHTMLX Gantt Packages + +Remove DevExpress dependencies: + +```bash +npm uninstall devextreme devextreme-react +``` + +Install DHTMLX React Gantt following the [installation guide](https://docs.dhtmlx.com/gantt/guides/installation/). + +For this tutorial, we will use the trial version of DHTMLX React Gantt: + +```bash +npm install @dhtmlx/trial-react-gantt +``` + +Install date formatting library for MySQL DATETIME conversion: + +```bash +npm install date-format-lite +``` + +### Add Data Loading Endpoint + +Add the GET endpoint to load data in DHTMLX format. Import the `date-format-lite` library at the top of your `server.js`: + +```js +import dateFormat from 'date-format-lite'; +``` + +Then add the data loading endpoint: + +```js +// GET /load - Load all tasks and links +app.get('/load', async (req, res) => { + try { + const [tasks] = await pool.query('SELECT * FROM gantt_tasks ORDER BY id'); + 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({ + data: tasks, + links: links, + }); + } catch (error) { + console.error('Error loading data:', error); + res.status(500).json({ error: 'Failed to load data' }); + } +}); +``` + +DevExpress returns separate arrays, DHTMLX expects `{ data: [...], links: [...] }`. + +### Add CRUD Endpoints for Tasks and Links + +DHTMLX React Gantt uses a custom save handler to synchronize data with the server. Each operation (create, update, delete) is sent with the appropriate HTTP method. + +Add handlers for task operations: + +```js +// POST /save/task - Create a new task +app.post('/save/task', async (req, res) => { + try { + const task = getTask(req.body); + + const [result] = await pool.query( + 'INSERT INTO gantt_tasks (text, start_date, end_date, progress, parent) VALUES (?, ?, ?, ?, ?)', + [task.text, task.start_date, task.end_date, task.progress, task.parent], + ); + + sendResponse(res, 'inserted', result.insertId); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// PUT /save/task/:id - Update an existing task +app.put('/save/task/:id', async (req, res) => { + try { + const taskId = req.params.id; + const task = getTask(req.body); + + await pool.query( + 'UPDATE gantt_tasks SET text = ?, start_date = ?, end_date = ?, progress = ?, parent = ? WHERE id = ?', + [task.text, task.start_date, task.end_date, task.progress, task.parent, taskId], + ); + + sendResponse(res, 'updated'); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// DELETE /save/task/:id - Delete a task +app.delete('/save/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: + +```js +// POST /save/link - Create new link +app.post('/save/link', async (req, res) => { + try { + const link = getLink(req.body); + + const [result] = await pool.query('INSERT INTO gantt_links (source, target, type) VALUES (?, ?, ?)', [ + link.source, + link.target, + link.type, + ]); + + sendResponse(res, 'inserted', result.insertId); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// PUT /save/link/:id - Update existing link +app.put('/save/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 /save/link/:id - Delete link +app.delete('/save/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 + +Add utility functions to process data and send responses: + +```js +// Helper: Parse task data from request +function getTask(data) { + return { + text: data.text, + start_date: data.start_date, + end_date: data.end_date, + progress: parseFloat(data.progress) || 0, + parent: data.parent || 0, + }; +} + +// Helper: Parse link data from request +function getLink(data) { + return { + source: data.source, + target: data.target, + type: data.type, + }; +} + +// Helper: Send response to DataProcessor +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); +} +``` + +--- + +## Step 3: Frontend Migration + +### Remove DevExpress Components and Services + +Delete CustomStore service file (`src/services/dataService.ts`) - DHTMLX React Gantt doesn't use CustomStore + +Remove DevExpress CSS links from `index.html` + +If you added DevExpress CSS links in your `index.html`, remove them: + +```html + + + +``` + +DHTMLX React Gantt includes its own styles, which are imported directly in the component: + +```typescript +import '@dhtmlx/trial-react-gantt/dist/react-gantt.css'; +``` + +### Update Vite Configuration + +Update your `vite.config.ts` to proxy API requests to the backend server. This is important for development mode: + +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +const PORT = process.env.PORT || 1337; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/load': { + target: `http://localhost:${PORT}`, + changeOrigin: true, + }, + '/save': { + target: `http://localhost:${PORT}`, + changeOrigin: true, + }, + }, + }, +}); +``` + +### Update package.json + +Make sure your `package.json` has the correct dependencies: + +```json +"dependencies": { + "@dhtmlx/trial-react-gantt": "^9.1.4", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "date-format-lite": "^17.7.0", + "dotenv": "^17.2.4", + "express": "^5.2.1", + "mysql2": "^3.16.3", + "nodemon": "^3.1.11", + "react": "^19.2.0", + "react-dom": "^19.2.0" +}, +"devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" +} +``` + +### Update src/App.tsx + +Replace your DevExpress Gantt component in `src/App.tsx` with DHTMLX React Gantt: + +```typescript +import { useCallback, useMemo, useRef } from 'react'; +import ReactGantt, { type GanttConfig, type Link, type ReactGanttRef, type Task } from '@dhtmlx/trial-react-gantt'; +import '@dhtmlx/trial-react-gantt/dist/react-gantt.css'; +import './App.css'; + +function App() { + const ganttRef = useRef(null); + + const config: GanttConfig = useMemo( + () => ({ + date_format: '%Y-%m-%d %H:%i:%s', + scales: [ + { unit: 'month', step: 1, format: '%F %Y' }, + { unit: 'week', step: 1, format: 'Week #%W' }, + ], + open_tree_initially: true, + }), + [] + ); + + const save = useCallback( + async (entity: string, action: 'update' | 'create' | 'delete', item: Task | Link, id: string | number) => { + switch (action) { + case 'create': + return await fetch(`/save/${entity}`, { + method: 'POST', + body: JSON.stringify(item), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => response.json()) + .then((result) => ({ id: result.tid })); + + case 'update': + await fetch(`/save/${entity}/${id}`, { + method: 'PUT', + body: JSON.stringify(item), + headers: { + 'Content-Type': 'application/json', + }, + }); + break; + + case 'delete': + await fetch(`/save/${entity}/${id}`, { + method: 'DELETE', + }); + break; + + default: + throw new Error(`Invalid action: ${action}`); + } + }, + [] + ); + + return ( + + ); +} + +export default App; +``` + +--- + +### Running the Application + +For development mode, you need to run two processes: + +Terminal 1 - Backend (Express): + +```bash +npm run server +``` + +This starts the API server on `http://localhost:1337` (or your configured PORT from `.env`) + +You should see: + +``` +Server is running on port 1337 +``` + +Terminal 2 - Frontend (Vite): + +```bash +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 API requests to the Express backend +automatically. + +You should see the DHTMLX Gantt chart with your data loaded from the database: + +![Gantt with data loaded](/img/migrating/devexpress/dhtmlx-gantt-data-loaded.png) + +### Explore DHTMLX Gantt Features + +- [DHTMLX Gantt documentation](https://docs.dhtmlx.com/gantt/) +- [API reference](https://docs.dhtmlx.com/gantt/api/api-overview/) +- [React Gantt configuration](https://docs.dhtmlx.com/gantt/integrations/react/configuration-props/) +- [React Gantt integration](https://docs.dhtmlx.com/gantt/integrations/react/) diff --git a/docs/migrating/from-frappe.md b/docs/migrating/from-frappe.md new file mode 100644 index 00000000..992ca4ef --- /dev/null +++ b/docs/migrating/from-frappe.md @@ -0,0 +1,667 @@ +--- +title: "Migrating from Frappe to DHTMLX Gantt" +sidebar_label: "From Frappe" +--- + +:::note +The complete demo source code is available on GitHub: [https://github.com/DHTMLX/gantt-migrating-from-frappe](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](https://frappe.io/gantt) to [DHTMLX Gantt](https://dhtmlx.com/docs/products/dhtmlxGantt/). 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](/img/migrating/frappe/frappe-tasks-table.png) + +### Create DHTMLX Tables + +DHTMLX Gantt uses two separate tables: one for tasks and one for dependency links. Create them in the same database: + +```sql +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:** + +```sql +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: + +```sql +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: + +```js +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`: + +```json +{ + "scripts": { + "migrate": "node migrate-frappe-to-dhtmlx.js" + } +} +``` + +Run the migration: + +```bash +npm run migrate +``` + +You can verify that the links were migrated correctly: + +```sql +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 Field | DHTMLX Field | Notes | +| ----------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `id` | `id` | Task id | +| `name` | `text` | Task name | +| `start` | `start_date` | The start date of the task | +| `end` | `end_date` | The end date of the task | +| _(not stored)_ | `duration` | The task duration. In DHTMLX Gantt, if not specified, Gantt will calculate it based on the `start_date` and `end_date` properties | +| `progress` | `progress` | Frappe: integer 0–100; DHTMLX: decimal 0.0–1.0 | +| _(not supported)_ | `parent` | Frappe 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](guides/installation.md). + +For this tutorial, we will use the trial version of DHTMLX Gantt: + +```bash +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: + +```js +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' }); + } +}); +``` + +### Add CRUD Endpoints for Tasks and Links + +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](guides/server-side.md). + +Replace the Frappe task endpoints (`POST /data/tasks`, `PUT /data/tasks/:id`, `DELETE /data/tasks/:id`) with: + +```js +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: + +```js +// 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: + +```js +// 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](guides/server-side.md#requestresponsedetails). + +## 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`: + +```js +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): + +```js +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: + +```html + + +``` + +Also remove the entire custom UI markup inside `` 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 `
` container + +Replace with a minimal structure: + +```html + + + + + + + DHTMLX Gantt Demo + + +
+ + + +``` + +### 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: + +```css +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: + +```js +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):** + +```bash +npm run server +``` + +This starts the API server on `http://localhost:1337` (or your configured port). + +**Terminal 2 — Frontend (Vite):** + +```bash +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](/img/migrating/frappe/dhtmlx-gantt-chart.png) + +## Next Steps + +- Explore [DHTMLX Gantt documentation](https://docs.dhtmlx.com/gantt/) for advanced features +- Review the [API reference](https://docs.dhtmlx.com/gantt/api__refs__gantt.html) for customization options +- Check out [DHTMLX Gantt samples](https://docs.dhtmlx.com/gantt/samples/) for implementation examples diff --git a/docs/migrating/from-syncfusion.md b/docs/migrating/from-syncfusion.md new file mode 100644 index 00000000..5369c2d9 --- /dev/null +++ b/docs/migrating/from-syncfusion.md @@ -0,0 +1,728 @@ +--- +title: "Migrating from Syncfusion to DHTMLX Gantt" +sidebar_label: "From Syncfusion" +--- + +:::note +The complete demo source code is available on GitHub: [https://github.com/DHTMLX/gantt-migrating-from-syncfusion](https://github.com/DHTMLX/gantt-migrating-from-syncfusion). +::: + +# Migrating from Syncfusion Gantt to DHTMLX Gantt + +## Introduction + +This guide will walk you through the process of migrating an existing application from [Syncfusion Gantt](https://www.syncfusion.com/javascript-ui-controls/js-gantt-chart) to [DHTMLX Gantt](https://dhtmlx.com/docs/products/dhtmlxGantt/). 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 Syncfusion Gantt +- Node.js (>= 20.0.0) installed +- MySQL database with Syncfusion data structure +- Basic knowledge of Express.js and JavaScript + +## Step 1: Database Migration + +### Understanding Current Schema + +If you followed the Syncfusion demo setup, you should have one table: `syncfusion_tasks`. + +The `syncfusion_tasks` table structure: + +![Syncfusion tasks table](/img/migrating/syncfusion/syncfusion-tasks-table.png) +![Syncfusion tasks table](/img/migrating/syncfusion/syncfusion-tasks-table2.png) + +### Create DHTMLX Tables + +Create two new tables compatible with DHTMLX Gantt: + +```sql +CREATE TABLE IF NOT EXISTS `gantt_tasks` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `text` VARCHAR(255) NOT NULL, + `start_date` DATETIME NOT NULL, + `end_date` DATETIME NOT NULL, + `duration` INT(11) NOT NULL, + `progress` FLOAT NOT NULL DEFAULT 0, + `parent` INT(11) NOT NULL DEFAULT 0, + `notes` TEXT NULL, + `open` BOOLEAN NOT NULL DEFAULT TRUE, + PRIMARY KEY (`id`) +); + +CREATE TABLE IF NOT EXISTS `gantt_links` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `source` INT(11) NOT NULL, + `target` INT(11) NOT NULL, + `type` VARCHAR(1) NOT NULL, + `lag` INT(11) DEFAULT 0, + PRIMARY KEY (`id`) +); +``` + +### Migrate Existing Data + +Now migrate your existing Syncfusion data to the new DHTMLX tables. + +**Migrate tasks:** + +```sql +INSERT INTO gantt_tasks (id, text, start_date, end_date, duration, progress, parent, notes, open) +SELECT + TaskID, + TaskName, -- TaskName → text + StartDate, + COALESCE(EndDate, + DATE_ADD(StartDate, INTERVAL Duration DAY)), -- Calculate end_date if missing + COALESCE(Duration, + DATEDIFF(EndDate, StartDate)), -- Calculate duration if missing + COALESCE(Progress, 0) / 100, -- Convert percentage (0-100) to decimal (0-1) + COALESCE(ParentId, 0), -- ParentId → parent (0 for root tasks) + info, -- info → notes + COALESCE(isExpand, TRUE) -- isExpand → open +FROM syncfusion_tasks; +``` + +**Migrate links (dependencies)** + +In Syncfusion Gantt's data structure, dependencies are stored as strings in the `Predecessor` column: + +- Format examples: `"5"`, `"3,4"`, `"5FS+2"`, `"7SS-1,8FF+3"`, `"2FS-5 days"` + +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 a task that the dependency will start from +- `target` - the id of a task that the dependency will end with. +- `type` - the dependency type: `"0"` (FS), `"1"` (SS), `"2"` (FF), `"3"` (SF) +- `lag` - optional task's lag + +We'll implement a Node.js migration script to parse Syncfusion's string format and convert it to DHTMLX's structured format. + +**Understanding Syncfusion Predecessor Format:** + +| Example | Meaning | DHTMLX Equivalent | +| -------------- | ----------------------------------- | ------------------------------- | +| `"5"` | Task depends on task 5 (default FS) | `source: 5, type: "0"` | +| `"3,4"` | Depends on tasks 3 AND 4 | Two separate links | +| `"5FS"` | Finish-to-Start dependency | `source: 5, type: "0"` | +| `"5FS+2"` | FS with 2 days positive lag | `source: 5, type: "0", lag: 2` | +| `"5FS-3"` | FS with 3 days negative lag | `source: 5, type: "0", lag: -3` | +| `"2FS-5 days"` | FS with lag including "days" text | `source: 2, type: "0", lag: -5` | + +Create a `migrate-dependencies.js` file and paste the following code into it: + +```js +import { pool } from './server.js'; + +const LINK_TYPE_MAP = { + FS: '0', // Finish-to-Start + SS: '1', // Start-to-Start + FF: '2', // Finish-to-Finish + SF: '3', // Start-to-Finish +}; + +/** + * Parse a single predecessor string like "5FS+2" or "7SS-1 days" + * @param {string} predecessor - Single predecessor string + * @returns {object|null} - Parsed link object or null if invalid + */ +function parseSinglePredecessor(predecessor) { + const clean = predecessor.trim(); + + // Regex pattern to match: TaskID [Type] [+/-Lag] + // Matches: "5", "5FS", "5FS+2", "7SS-1", "3FS+2 days", "8SS-1 days" + const pattern = /^(\d+)(FS|SS|FF|SF)?([\+\-]\d+)?(?:\s+days?)?$/i; + const match = clean.match(pattern); + + if (!match) { + console.warn(`Cannot parse predecessor: "${predecessor}"`); + return null; + } + + const source = parseInt(match[1]); + const typeCode = match[2] ? match[2].toUpperCase() : 'FS'; + const lag = match[3] ? parseInt(match[3]) : 0; + + return { + source, + type: LINK_TYPE_MAP[typeCode] || '0', + lag, + }; +} + +/** + * Parse a full predecessor string that may contain multiple dependencies + * @param {string} predecessorString - Full predecessor string from database (e.g., "3,4FS+2,5SS-1") + * @returns {array} - Array of link objects + */ +function parsePredecessors(predecessorString) { + if (!predecessorString || predecessorString.trim() === '') { + return []; + } + + const parts = predecessorString.split(','); + const links = []; + + for (const part of parts) { + const link = parseSinglePredecessor(part); + if (link) { + links.push(link); + } + } + + return links; +} + +async function migrateDependencies() { + const connection = await pool.getConnection(); + + try { + console.log('Starting dependency migration...\n'); + + // Step 1: Query all tasks that have predecessors + const [tasks] = await connection.query( + 'SELECT TaskID, TaskName, Predecessor FROM syncfusion_tasks WHERE Predecessor IS NOT NULL AND Predecessor != ""' + ); + + console.log(`Found ${tasks.length} tasks with predecessors\n`); + + const linksToInsert = []; + let skippedCount = 0; + + // Step 2: Parse each task's predecessor string + for (const task of tasks) { + const targetId = task.TaskID; + const predecessorString = task.Predecessor; + const links = parsePredecessors(predecessorString); + + if (links.length === 0) { + console.log(`No valid links parsed`); + skippedCount++; + continue; + } + + // Step 3: Create link objects for insertion + for (const link of links) { + linksToInsert.push({ + source: link.source, + target: targetId, + type: link.type, + lag: link.lag || 0, + }); + + const typeName = Object.keys(LINK_TYPE_MAP).find((key) => LINK_TYPE_MAP[key] === link.type); + console.log(`Link: ${link.source} -> ${targetId} (${typeName})`); + } + } + + console.log(`\n--- Summary ---`); + console.log(`Tasks processed: ${tasks.length}`); + console.log(`Links to create: ${linksToInsert.length}`); + console.log(`Tasks skipped: ${skippedCount}\n`); + + // Step 4: Insert links into database (with transaction) + if (linksToInsert.length > 0) { + await connection.beginTransaction(); + + try { + // Clear existing links to avoid duplicates + await connection.query('DELETE FROM gantt_links'); + console.log('Cleared existing links from gantt_links table'); + + // Insert each link + for (const link of linksToInsert) { + await connection.query('INSERT INTO gantt_links (source, target, type, `lag`) VALUES (?, ?, ?, ?)', [ + link.source, + link.target, + link.type, + link.lag, + ]); + } + + await connection.commit(); + console.log(`Successfully inserted ${linksToInsert.length} links\n`); + + const [insertedLinks] = await connection.query('SELECT * FROM gantt_links ORDER BY id'); + console.log('Inserted links:'); + console.table(insertedLinks); + } catch (error) { + await connection.rollback(); + throw error; + } + } + + console.log('\nMigration completed successfully!'); + } catch (error) { + console.error('Error during migration:', error); + throw error; + } finally { + connection.release(); + await pool.end(); + } +} + +// Run the migration +migrateDependencies().catch(console.error); +``` + +Then add a script to your `dhtmlx-demo/package.json`: + +```json +{ + "scripts": { + "migrate-deps": "node migrate-dependencies.js" + } +} +``` + +Run the migration: + +```bash +cd dhtmlx-demo +npm run migrate-deps +``` + +You can verify that the data was migrated correctly by running the following commands: + +```sql +SELECT * FROM gantt_tasks; +SELECT * FROM gantt_links; +``` + +You should see all your tasks and links properly transferred with the correct field mappings. + +### Mapping Syncfusion Task Fields to DHTMLX Gantt + +| Syncfusion Field | DHTMLX Field | Notes | +| ---------------- | --------------- | --------------- | +| `TaskID` | `id` | Task id | +| `TaskName` | `text` | Task name | +| `StartDate` | `start_date` | Task start date | +| `EndDate` | `end_date` | Task end date (calculated in DHTMLX if not provided) | +| `Duration` | `duration` | Task duration | +| `DurationUnit` | _(config)_ | DHTMLX Gantt uses a global duration unit configured via `gantt.config.duration_unit`. During migration, it's recommended to normalize all durations to a single unit. If you want to have different duration units for different tasks, i.e. to show durations of some tasks in hours and some tasks in "days", you can use the [formatter module](guides/working-time.md#task-duration-in-decimal-format-taskdurationindecimalformat). | +| `Progress` | `progress` | Syncfusion: 0-100%, DHTMLX: 0-1 (decimal) | +| `ParentId` | `parent` | Parent task ID (0 for root tasks) | +| `Predecessor` | _(links table)_ | Syncfusion stores as string, DHTMLX uses separate `gantt_links` table | +| `info` (notes) | - | Can be added as a custom column. Check this article for more information: [How to add a custom column in the grid](guides/how-to.md/#how-to-add-a-custom-column-in-the-grid) | +| `isExpand` | `open` | Expand/collapse state for parent tasks | +| `Indicators` | `markers` | DHTMLX uses `gantt.addMarker()` API. Learn more about [adding vertical markers](guides/markers.md) | + +## Step 2: Backend Migration (server.js) + +### Remove Syncfusion Endpoints + +Delete the following Syncfusion-specific endpoints from your `server.js`: + +- `app.post('/api/getTasks', ...)` - Syncfusion data loading endpoint +- `app.post('/api/batchTasks', ...)` - Syncfusion batch sync endpoint + +### Install DHTMLX Gantt Package and Vite + +Remove Syncfusion dependency: + +```bash +npm uninstall @syncfusion/ej2 +``` + +Install DHTMLX Gantt following the [installation guide](guides/installation.md). + +For this tutorial, we will use the trial version of DHTMLX Gantt: + +```bash +npm install @dhx/trial-gantt +``` + +Let's also install Vite as a build tool: + +```bash +npm install --save-dev vite +``` + +### Add Data Loading Endpoint + +We'll use the `date-format-lite` library to format dates from MySQL DATETIME format to the format expected by DHTMLX. + +Install the library: + +```bash +npm install date-format-lite +``` + +Add the GET endpoint to load data in DHTMLX format: + +```js +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 id'); + 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' }); + } +}); +``` + +**Note:** The response format is different from Syncfusion (`{ result: [...], count: number }`). DHTMLX expects `{ tasks: [], links: [] }`. + +### Add CRUD Endpoints for Tasks and Links + +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 with the appropriate method. +Learn more about [Server-side integration](guides/server-side.md). + +Add handlers for **task operations**: + +```js +// Create a new task +app.post('/data/task', async (req, res) => { + try { + const task = getTask(req.body); + + const [result] = await pool.query( + `INSERT INTO gantt_tasks (text, start_date, end_date, duration, progress, parent, notes) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [task.text, task.start_date, task.end_date, task.duration, task.progress, task.parent, task.notes] + ); + sendResponse(res, 'inserted', result.insertId); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// Update an existing task +app.put('/data/task/:id', async (req, res) => { + try { + const taskId = req.params.id; + const task = getTask(req.body); + await pool.query( + `UPDATE gantt_tasks + SET text = ?, start_date = ?, end_date = ?, duration = ?, progress = ?, parent = ?, notes = ? + WHERE id = ?`, + [task.text, task.start_date, task.end_date, task.duration, task.progress, task.parent, task.notes, taskId] + ); + sendResponse(res, 'updated'); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// 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: + +```js +// POST /data/link - Create new link +app.post('/data/link', async (req, res) => { + try { + const link = getLink(req.body); + const [result] = await pool.query('INSERT INTO gantt_links (source, target, type) VALUES (?, ?, ?)', [ + link.source, + link.target, + link.type, + ]); + sendResponse(res, 'inserted', result.insertId); + } catch (error) { + sendResponse(res, 'error', null, error); + } +}); + +// PUT /data/link/:id - Update 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 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 + +Also, let's add utility functions to process data and send responses: + +```js +// Helper: Parse task data from request +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, + notes: data.notes || null, + }; +} + +// Helper: Parse link data from request +function getLink(data) { + return { + source: data.source, + target: data.target, + type: data.type, + }; +} + +// Helper: Send response to DataProcessor +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); +} +``` + +--- + +## Step 3: Frontend Migration with Vite + +### Set Up Vite Configuration + +Create a `vite.config.js` file in the root of your project: + +```javascript +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: './src', + server: { + port: 5173, + proxy: { + '/data': { + target: 'http://localhost:1337', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); +``` + +Organize your project with this structure: + +``` +dhtmlx-demo/ +├── src/ # Frontend source code +│ ├── app/ +│ │ └── app.ts # Main Gantt initialization +│ ├── index.html # Main HTML file +│ ├── resources/ +│ └── styles/ +├── e2e/ # End-to-end tests (optional) +├── .env.example +├── .gitignore +├── migrate-dependencies.js # Dependency migration script +├── package.json # Project dependencies +├── server.js # Express server +├── setup.sql # Database setup script +├── tsconfig.json # TypeScript configuration +└── vite.config.js # Vite configuration +``` + +### Update index.html + +Update `index.html` with the following code: + +```html + + + + DHTMLX Gantt Chart Demo + + + + + + + + + +
+

DHTMLX Gantt Chart - MySQL Integration Demo

+
+
+ + + + +``` + +**Note:** The container ID changed to `gantt_here`, which is DHTMLX Gantt's conventional container ID. + +### Update src/app/app.ts + +In the `src/app/app.ts` file, remove all Syncfusion-related imports and code. + +Replace with DHTMLX Gantt initialization: + +```ts +import '@dhx/trial-gantt/codebase/dhtmlxgantt.css'; +import { Gantt } from '@dhx/trial-gantt'; + +const gantt = Gantt.getGanttInstance(); + +gantt.config.date_format = '%Y-%m-%d %H:%i:%s'; +gantt.config.scale_height = 50; + +gantt.config.scales = [ + { unit: 'month', step: 1, format: '%F %Y' }, + { unit: 'day', step: 1, format: '%d' }, +]; + +gantt.config.lightbox.sections = [ + { name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true }, + { name: 'time', height: 72, type: 'duration', map_to: 'auto' }, + { name: 'notes', height: 70, map_to: 'notes', type: 'textarea' }, +]; + +gantt.init('gantt_here'); +gantt.load('/data'); + +const dp = gantt.createDataProcessor({ + url: '/data', // Base URL for REST endpoints + mode: 'REST', // Use RESTful mode +}); +``` + +The DataProcessor will automatically: + +- Send POST requests to `/data/task` when creating tasks +- Send PUT requests to `/data/task/:id` when updating tasks +- Send DELETE requests to `/data/task/:id` when deleting tasks +- Handle links similarly with `/data/link` endpoints + +### Update package.json Scripts + +Update your `package.json` scripts to use Vite: + +```json +{ + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "server": "nodemon server.js", + + "serve": "gulp e2e-serve", + "test": "gulp e2e-test", + "migrate-deps": "node migrate-dependencies.js", + "update-webdriver": "gulp e2e-webdriver-update" + } +} +``` + +--- + +## Step 4: Testing the Migration + +### Running the Application + +For development mode, you need to run two processes: + +**Terminal 1 - Backend (Express):** + +```bash +npm run server +``` + +This starts the API server on `http://localhost:1337` (or your configured port) + +**Terminal 2 - Frontend (Vite):** + +```bash +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 API requests to the Express backend automatically. + +You should see the DHTMLX Gantt chart with your data loaded from the database: + +![Gantt with data loaded](/img/migrating/syncfusion/dhtmlx-gantt-data-loaded.png) + +## Next Steps + +- Explore [DHTMLX Gantt documentation](https://docs.dhtmlx.com/gantt/) for advanced features +- Review the [API reference](https://docs.dhtmlx.com/gantt/api__refs__gantt.html) for customization options +- Check out [DHTMLX Gantt samples](https://docs.dhtmlx.com/gantt/samples/) for implementation examples diff --git a/docs/migrating/index.md b/docs/migrating/index.md new file mode 100644 index 00000000..cc435fcd --- /dev/null +++ b/docs/migrating/index.md @@ -0,0 +1,21 @@ +--- +title: "Migrating to DHTMLX Gantt" +sidebar_label: "Migrating" +--- + +# Migrating to DHTMLX Gantt + +These tutorials help you move an existing Gantt application from another JavaScript library to [DHTMLX Gantt](https://dhtmlx.com/docs/products/dhtmlxGantt/). Each guide walks through database schema changes, server-side API updates, and client-side integration, with a companion demo repository that includes both the original and migrated implementations. + +## Available migration guides + +- [Migrating from Bryntum to DHTMLX Gantt](./from-bryntum) +- [Migrating from Syncfusion to DHTMLX Gantt](./from-syncfusion) +- [Migrating from DevExpress to DHTMLX Gantt](./from-devexpress) +- [Migrating from Frappe to DHTMLX Gantt](./from-frappe) + +## Before you start + +- Node.js 20+ and a MySQL database are used in the demo projects. +- Install DHTMLX Gantt following the [installation guide](/guides/installation). +- For server integration patterns, see [Server-Side Integration](/guides/server-side). diff --git a/sidebars.js b/sidebars.js index 9549b9bf..a4e1d438 100644 --- a/sidebars.js +++ b/sidebars.js @@ -1615,6 +1615,20 @@ module.exports = { "guides/multiuser-live-updates", ] }, + { + type: "category", + label: "Migrating", + link: { + type: "doc", + id: "migrating/index", + }, + items: [ + "migrating/from-bryntum", + "migrating/from-syncfusion", + "migrating/from-devexpress", + "migrating/from-frappe", + ], + }, "guides/extensions-list", "guides/overview" ] diff --git a/static/img/migrating/bryntum/bryntum-deps-mysql.png b/static/img/migrating/bryntum/bryntum-deps-mysql.png new file mode 100644 index 00000000..03875d03 Binary files /dev/null and b/static/img/migrating/bryntum/bryntum-deps-mysql.png differ diff --git a/static/img/migrating/bryntum/bryntum-tasks-mysql.png b/static/img/migrating/bryntum/bryntum-tasks-mysql.png new file mode 100644 index 00000000..0e0c9867 Binary files /dev/null and b/static/img/migrating/bryntum/bryntum-tasks-mysql.png differ diff --git a/static/img/migrating/bryntum/gantt-data-loaded.png b/static/img/migrating/bryntum/gantt-data-loaded.png new file mode 100644 index 00000000..b4e5d718 Binary files /dev/null and b/static/img/migrating/bryntum/gantt-data-loaded.png differ diff --git a/static/img/migrating/devexpress/devexpress-links-table.png b/static/img/migrating/devexpress/devexpress-links-table.png new file mode 100644 index 00000000..56cf34d2 Binary files /dev/null and b/static/img/migrating/devexpress/devexpress-links-table.png differ diff --git a/static/img/migrating/devexpress/devexpress-tasks-table.png b/static/img/migrating/devexpress/devexpress-tasks-table.png new file mode 100644 index 00000000..f7351c6d Binary files /dev/null and b/static/img/migrating/devexpress/devexpress-tasks-table.png differ diff --git a/static/img/migrating/devexpress/dhtmlx-gantt-data-loaded.png b/static/img/migrating/devexpress/dhtmlx-gantt-data-loaded.png new file mode 100644 index 00000000..f556232d Binary files /dev/null and b/static/img/migrating/devexpress/dhtmlx-gantt-data-loaded.png differ diff --git a/static/img/migrating/frappe/dhtmlx-gantt-chart.png b/static/img/migrating/frappe/dhtmlx-gantt-chart.png new file mode 100644 index 00000000..05b90eb4 Binary files /dev/null and b/static/img/migrating/frappe/dhtmlx-gantt-chart.png differ diff --git a/static/img/migrating/frappe/frappe-tasks-table.png b/static/img/migrating/frappe/frappe-tasks-table.png new file mode 100644 index 00000000..bc9201c7 Binary files /dev/null and b/static/img/migrating/frappe/frappe-tasks-table.png differ diff --git a/static/img/migrating/syncfusion/dhtmlx-gantt-data-loaded.png b/static/img/migrating/syncfusion/dhtmlx-gantt-data-loaded.png new file mode 100644 index 00000000..8ad7dd7a Binary files /dev/null and b/static/img/migrating/syncfusion/dhtmlx-gantt-data-loaded.png differ diff --git a/static/img/migrating/syncfusion/syncfusion-tasks-table.png b/static/img/migrating/syncfusion/syncfusion-tasks-table.png new file mode 100644 index 00000000..7235c617 Binary files /dev/null and b/static/img/migrating/syncfusion/syncfusion-tasks-table.png differ diff --git a/static/img/migrating/syncfusion/syncfusion-tasks-table2.png b/static/img/migrating/syncfusion/syncfusion-tasks-table2.png new file mode 100644 index 00000000..dfeecc21 Binary files /dev/null and b/static/img/migrating/syncfusion/syncfusion-tasks-table2.png differ