dhtmlxGantt с Salesforce LWC

Этот гид описывает процесс интеграции dhtmlxGantt в Salesforce Lightning Web Component.

Если вы работаете с другой технологией, существуют иные варианты интеграции, которые вы можете изучить:

Для этого руководства мы будем использовать Salesforce CLI для создания Lightning Web Component и его развертывания в организации. Также может быть полезным установить Salesforce Extension Pack для Visual Studio Code, если вы работаете с организациями разработки.

Полный исходный код этого примера доступен на GitHub.

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

Предварительные требования

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


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

Если у вас еще нет аккаунта разработчика, вы можете зарегистрироваться для бесплатного. Руководство по созданию аккаунта можно найти здесь.

Начните с включения Dev Hub. Используйте строку поиска слева, чтобы найти и выбрать Dev Hub:

В окне настроек активируйте Enable 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 в ваш "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 — это Задачи и Связи. Практичным подходом является хранение всех свойств этих сущностей в виде простого JSON в Salesforce. Начните с создания объектов Задач и Связей. Откройте Object Manager, нажмите "Create" и выберите "Custom Object":

Объект Задачи

Назовите объект задачи GanttTask/GanttTasks:

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

Имя объекта: GanttTask => Имя записи: GanttTask Name

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

После создания объекта переключитесь на вкладку "Fields & Relationships" и нажмите "New":

Добавление полей

  • Duration

    • Выберите "Number" как тип данных и перейдите к следующему шагу:

    • Назовите поле "Duration". Оно будет хранить сериализованные в JSON свойства задачи. Продолжайте нажимать "Next", пока не сможете выбрать "Save & New":

  • Parent

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

    • Продолжайте с настройками по умолчанию, пока не сможете выбрать "Save & New".
  • Progress

    • Создайте поле "Progress" с типом данных "Number":

    • Продолжайте с настройками по умолчанию, пока не сможете выбрать "Save & New".
  • Start Date

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

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

В итоге должно получиться что-то вроде этого:

Объект Связи

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

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

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

Имя объекта: GanttLink => Имя записи: GanttLink Name

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

  • Source

Создайте поле с именем "Source" и выберите "Text" как тип данных.

Нажмите "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 Component и моделью данных:

$ 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 в списке. Добавьте его в регион и сохраните.

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

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

Откройте страницу приложения. Вы можете найти ее в лаунчере приложений, введя "Gantt".

Если все настроено правильно, вы увидите простую демонстрацию Ганта, работающую на Lightning Page.


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

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


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

Если задачи и связи не отображаются на странице после завершения настройки, ознакомьтесь со статьей Troubleshooting Backend Integration Issues. Она содержит рекомендации по диагностике и разрешению возможных проблем.


Что дальше

Теперь у вас есть работающий компонент Gantt. Полный код доступен на GitHub. Вы можете клонировать или загрузить его для своих проектов.

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

К началу