dhtmlxGantt с Salesforce LWC

В этом руководстве описывается, как интегрировать dhtmlxGantt в Salesforce Lightning Web Component.

Если вы работаете с другой технологией, ниже представлены другие варианты интеграции:

Процесс включает использование Salesforce CLI для создания Lightning Web Component и его деплоя в организацию Salesforce. Для более удобной разработки рекомендуется установить Salesforce Extension Pack в Visual Studio Code.

Полный исходный код размещён на GitHub.

Также доступен видеоурок, демонстрирующий создание диаграммы Gantt с помощью Salesforce LWC.

Требования

Убедитесь, что Salesforce CLI установлен. Если нет, воспользуйтесь этой инструкцией для установки.

Шаг 1. Создание проекта

Если у вас ещё нет аккаунта, создайте бесплатную учётную запись разработчика, зарегистрировавшись. Подробнее — в этом руководстве.

В Salesforce воспользуйтесь строкой поиска слева для поиска и выбора Dev Hub:

На открывшейся странице настроек включите функцию Dev Hub:

Затем создайте директорию для вашего проекта Salesforce DX:

$ mkdir ~/salesforce

Сгенерируйте проект Salesforce DX с помощью CLI:

$ cd ~/salesforce
$ sfdx project generate -n gantt-salesforce-app  
    target dir = C:\Users\User\salesforce
        create gantt-salesforce-app\config\project-scratch-def.json
        create gantt-salesforce-app\README.md
        create gantt-salesforce-app\sfdx-project.json
        create gantt-salesforce-app\.husky\pre-commit
        create gantt-salesforce-app\.vscode\extensions.json
        create gantt-salesforce-app\.vscode\launch.json
        create gantt-salesforce-app\.vscode\settings.json
        create gantt-salesforce-app\force-app\main\default\lwc\.eslintrc.json
        create gantt-salesforce-app\force-app\main\default\aura\.eslintrc.json
        create gantt-salesforce-app\scripts\soql\account.soql
        create gantt-salesforce-app\scripts\apex\hello.apex
        create gantt-salesforce-app\.eslintignore
        create gantt-salesforce-app\.forceignore
        create gantt-salesforce-app\.gitignore
        create gantt-salesforce-app\.prettierignore
        create gantt-salesforce-app\.prettierrc
        create gantt-salesforce-app\jest.config.js
        create gantt-salesforce-app\package.json

Перейдите в только что созданную папку проекта:

$ cd gantt-salesforce-app

Шаг 2. Авторизация

Используйте Web Server Flow для авторизации в организации:

$ sfdx org login web -d
 
Successfully authorized ... with org ID ...

Далее обновите файл конфигурации проекта (sfdx-project.json), установив параметр "sfdcLoginUrl" на "My Domain URL" вашей организации. Найти этот URL можно на странице настроек "My Domain". Например:

gantt-salesforce-app/sfdx-project.json

"sfdcLoginUrl" : "https://xbs2-dev-ed.my.salesforce.com"

Создайте Scratch Org с помощью следующей команды:

$ sfdx org create scratch -f config/project-scratch-def.json -d
 
Creating Scratch Org...
RequestId: 2SR5j0000006JhCGAU 
(https://xbsoftware2-dev-ed.my.salesforce.com/2SR5j0000006JhCGAU)
OrgId: 00DH40000000s0D
Username: test-tc0telfqhudt@example.com
✓ Prepare Request
✓ Send Request
✓ Wait For Org
✓ Available
✓ Authenticate
✓ Deploy Settings
Done
 
Your scratch org is ready.

Шаг 3. Добавление Gantt в Salesforce

Чтобы использовать библиотеку, её необходимо загрузить в Salesforce как Static Resource. Откройте ваш scratch org:

$ sfdx org open

Перейдите на вкладку "Static Resources" и нажмите кнопку "New":

Укажите понятное имя (например, "dhtmlxgantt7111"), загрузите ZIP-архив с файлами библиотеки (dhtmlxgantt.js и dhtmlxgantt.css), и установите Cache Control в значение "Public" для повышения производительности. Затем сохраните изменения.

Теперь библиотека dhtmlxGantt доступна внутри Salesforce.

Шаг 4. Создание модели данных

Основные компоненты dhtmlxGantt — это задачи (Tasks) и связи (Links). Практичный способ их хранения — сохранять их свойства в формате JSON в Salesforce. Начните с создания пользовательских объектов для задач и связей. Перейдите в Object Manager, затем выберите "Create" и выберите "Custom Object":

Объект задачи (Task object)

Назовите объект задачи, например, GanttTask или GanttTasks.

Убедитесь, что имя записи соответствует имени объекта, например:

Object Name: GanttTask => Record Name: GanttTask Name

Сохраните новый объект.

Затем откройте вкладку "Fields & Relationships" и нажмите "New" для добавления полей:

  • Duration

Выберите тип данных "Number" и продолжайте.

Назовите поле "Duration". Это поле будет содержать сериализованные в JSON свойства задачи. Нажимайте "Next" до появления кнопки "Save & New".

Примите настройки по умолчанию, нажимая "Next", пока не сможете сохранить или добавить новое поле.

  • Parent

Создайте поле "Parent" с типом данных "Text".

Проходите шаги, нажимая "Next", до появления кнопки "Save & New".

  • Progress

Добавьте поле "Progress", выбрав тип данных "Number".

Продолжайте нажимать "Next", пока не сможете сохранить или добавить следующее поле.

  • Start date

Создайте поле "Start Date" с типом данных "Date/Time".

Пройдите по умолчанию все шаги до кнопки "Save".

В результате поля вашего объекта должны выглядеть так:

Объект ссылок

Начните с открытия Object Manager и выберите "Create", затем "Custom Object":

Назовите объект ссылок как GanttLink/GanttLinks.

Убедитесь, что имя записи соответствует имени объекта, например:

Object Name: GanttLink => Record Name: GanttLink Name

Далее создайте необходимые поля.

  • Source

Добавьте поле "Source" и выберите "Text" в качестве Data Type.

Нажимайте "Next" (оставляя настройки по умолчанию), пока не появится кнопка "Save & New".

  • Target

Добавьте поле "Target" с типом данных "Text".

Нажимайте "Next" (принимая настройки по умолчанию), пока не станет доступна кнопка "Save & New".

  • Type

Добавьте поле "Type", также с типом данных "Text".

Нажимайте "Next" (принимая значения по умолчанию), пока не появится кнопка "Save".

В итоге должно получиться следующее:

Шаг 5. Создание Lightning Web Component

Для создания Lightning Web Component выполните следующую команду:

$ sfdx lightning generate component --type lwc -n gantt -d force-app/main/default/lwc
 
target dir = 
C:\Users\User\source\salesforce\gantt-salesforce-app\force-app\main\default\lwc
   create force-app\main\default\lwc\gantt\gantt.js
   create force-app\main\default\lwc\gantt\gantt.html
   create force-app\main\default\lwc\gantt\gantt.js-meta.xml

Обновите определение компонента в gantt.js-meta.xml, чтобы сделать его доступным в Lightning App Builder:

force-app/main/default/lwc/gantt/gantt.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AppPage">
            <property name="height" label="Height" type="Integer" default="800" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Откройте gantt.html и вставьте следующий код:

force-app/main/default/lwc/gantt/gantt.html

<template>
    <div class="thegantt" lwc:dom="manual" style='width: 100%;'></div>
</template>

В gantt.js добавьте следующий код:

force-app/main/default/lwc/gantt/gantt.js

/* eslint-disable guard-for-in */
/* eslint-disable no-undef */
import { LightningElement, api } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadStyle, loadScript } from "lightning/platformResourceLoader";
import { createRecord, updateRecord, deleteRecord } from "lightning/uiRecordApi";
 
// Static resources
import GanttFiles from "@salesforce/resourceUrl/dhtmlxgantt7111";
 
// Controllers
import getTasks from "@salesforce/apex/GanttData.getTasks";
 
function unwrap(fromSF) {
    const data = fromSF.tasks.map((a) => ({
        id: a.Id,
        text: a.Name,
        start_date: a.Start_Date__c,
        duration: a.Duration__c,
        parent: a.Parent__c,
        progress: a.Progress__c,
        type: a.Task_Type__c,
    }));
    const links = fromSF.links.map((a) => ({
        id: a.Id,
        source: a.Source__c,
        target: a.Target__c,
        type: a.Type__c
    }));
    return { data, links};
}
 
export default class GanttView extends LightningElement {
    static delegatesFocus = true;
 
    @api height;
    ganttInitialized = false;
 
    renderedCallback() {
        if (this.ganttInitialized) {
            return;
        }
        this.ganttInitialized = true;
 
        Promise.all([
            loadScript(this, GanttFiles + "/dhtmlxgantt.js"),
            loadStyle(this, GanttFiles + "/dhtmlxgantt.css")
        ])
            .then(() => {
                this.initializeUI();
            })
            .catch((error) => {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "Error loading Gantt",
                        message: error.message,
                        variant: "error"
                    })
                );
            });
    }
 
    initializeUI() {
        const root = this.template.querySelector(".thegantt");
        root.style.height = this.height + "px";
 
        //uncomment the following line if you use the Enterprise or Ultimate version
        //const gantt = window.Gantt.getGanttInstance();
        gantt.templates.parse_date = (date) => new Date(date);
        gantt.templates.format_date = (date) => date.toISOString();
 
        gantt.init(root);
        getTasks().then((d) => {
            const chartData = unwrap(d);
            gantt.parse({
                tasks: chartData.data,
                links: chartData.links
            });
        });
 
        ///↓↓↓ saving changes back to SF backend ↓↓↓
        gantt.createDataProcessor({
            task: {
                create: (data) => {
                    console.log("createTask",data);
                    const insert = {
                        apiName: "GanttTask__c",
                        fields: {
                            Name: data.text,
                            Start_Date__c: data.start_date,
                            Duration__c: data.duration,
                            Parent__c: String(data.parent),
                            Progress__c: data.progress
                        }
                    };
                    gantt.config.readonly = true; // suppress changes
                                                  // until saving is complete  
                    return createRecord(insert).then((res) => {
                        gantt.config.readonly = false;
                        return { tid: res.id, ...res };
                    });
                },
                update: (data, id) => {
                    console.log("updateTask",data);
                    const update = {
                        fields: {
                            Id: id,
                            Name: data.text,
                            Start_Date__c: data.start_date,
                            Duration__c: data.duration,
                            Parent__c: String(data.parent),
                            Progress__c: data.progress
                        }
                    };
                    return updateRecord(update).then(() => ({}));
                },
                delete: (id) => {
                    return deleteRecord(id).then(() => ({}));
                }
            },
            link: {
                create: (data) => {
                    const insert = {
                        apiName: "GanttLink__c",
                        fields: {
                            Source__c: data.source,
                            Target__c: data.target,
                            Type__c: data.type
                        }
                    };
                    return createRecord(insert).then((res) => {
                        return { tid: res.id };
                    });
                },
                update: (data, id) => {
                    const update = {
                        apiName: "GanttLink__c",
                        fields: {
                            Id: id,
                            Source__c: data.source,
                            Target__c: data.target,
                            Type__c: data.type
                        }
                    };
                    return updateRecord(update).then(() => ({}));
                },
                delete: (id) => {
                    return deleteRecord(id).then(() => ({}));
                }
            }
        });
    }
}

Шаг 6. Создание Apex класса

Далее создайте класс, который будет обеспечивать связь между Lightning компонентом и моделью данных.

$ sfdx apex generate class -n GanttData -d force-app/main/default/classes
 
target dir = 
C:\Users\User\salesforce\gantt-salesforce-app\force-app\main\default\classes
   create force-app\main\default\classes\GanttData.cls
   create force-app\main\default\classes\GanttData.cls-meta.xml

После создания класса откройте GanttData.cls и добавьте следующий код:

force-app/main/default/classes/GanttData.cls

public with sharing class GanttData {
 
    @RemoteAction
    @AuraEnabled(cacheable=true)
    public static Map<String, Object> getTasks() {
 
        // fetching the Records via SOQL
        List<GanttTask__c> Tasks = new List<GanttTask__c>();
        Tasks = [SELECT Id, Name, Start_Date__c, Duration__c, 
                    Parent__c FROM GanttTask__c];
 
        List<GanttLink__c> Links = new List<GanttLink__c>();
        Links = [SELECT Id, Type__c, Source__c, Target__c FROM GanttLink__c];
 
        Map<String, Object> result = new Map<String, Object>{
            'tasks' => Tasks, 'links' => Links };
        return result;
   }
}

Выполните команду для получения исходников из Scratch Org в ваш проект:

$ sfdx project retrieve start

Затем задеплойте исходники обратно в Scratch Org:

$ sfdx project deploy start

Шаг 7. Создание Lightning Page

Откройте "Lightning App Builder" и создайте новую Lightning Page.

Выберите “App Page” и укажите имя страницы и макет.

Кастомный компонент Gantt должен быть доступен для новой страницы. Добавьте его в любой раздел и сохраните.

Активируйте страницу.

Сохраните изменения.

Откройте страницу приложения. Она будет доступна через app launcher по поиску Gantt.

Если всё настроено верно, на Lightning Page появится простой пример Gantt.

Безопасность приложения

Сам Gantt не содержит встроенных средств защиты от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. За безопасность приложения отвечают разработчики, внедряющие его. Подробнее см. в соответствующей статье. Salesforce предоставляет мощные инструменты для защиты ваших данных и приложений. Вы также можете адаптировать подход к безопасности под структуру и требования вашей организации. Дополнительные рекомендации смотрите в Salesforce Security Guide. Информация по защите Lightning компонентов доступна здесь.

Устранение неполадок

Если вы выполнили все шаги, но диаграмма Gantt не отображает задачи и связи на странице, обратитесь к статье Решение проблем с интеграцией бэкенда. В ней описаны методы диагностики и устранения типовых проблем.

Что дальше

Когда Gantt полностью настроен, вы можете ознакомиться с полным кодом на GitHub, где его можно клонировать или скачать для поддержки ваших проектов.

Также рекомендуем изучить руководства по различным функциям Gantt или туториалы по интеграции Gantt с другими backend-фреймворками.

К началу