Получил достаточно аплодисментов (на самом деле один) для первой части, так что, похоже, мне нужно раскрыть больше секретов создания Workflow Editor. Пожалуйста, начните с первой части, если вы ее пропустили.

В RequestLogic у нас есть простой редактор рабочего процесса, в котором нельзя создавать циклы, только последовательные события и переходы с помощью простых операторов if.

В RequestLogic есть 3 типа узлов: отправка электронной почты, изменение состояния и условие. Первые два являются одношаговыми процессами, узел Условие может разветвляться в зависимости от разных условий. В настоящее время статус последней отправленной почты можно наблюдать в состоянии «отправлено», «открыто», «нажато» или «отписано».

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

Узлы следуют друг за другом в последовательном порядке, и только узел условия может немного встряхнуть его.

На этот раз я хотел бы создать или хотя бы написать о создании более общего редактора рабочего процесса, который позволяет подключить любую из нод и позволить обрабатывать задачи параллельно.

Сделайте это сложным

Если вы следовали первой части (ссылка здесь, если вы пропустили не только первую часть, но и первую часть этой статьи), у вас уже должен быть статический редактор, который можно модифицировать, изменив способ создания узлов. в массиве узлов.

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

export class Node {
  constructor(
    public id: string,
    public relationIds: string[] = []
  ) {
  }
}
var nodes = [];
nodes.push(new Node("root"));
nodes.push(new Node("Node A", ["root"]));
nodes.push(new Node("Node B", ["root"]));
nodes.push(new Node("Node C", ["root"]));
nodes.push(new Node("Node D", ["Node B", "Node C", "root"]));

Мы должны изменить способ передачи ребер Дагре следующим образом:

nodes.forEach(node => {
  g.setNode(node.id, { width: 200, height: 40});
  node.relationIds.forEach(rel => {
    g.setEdge(rel, node.id, {});
  });
});

Функции редактирования

А теперь некоторые возможности редактирования. Следующие 3 являются минимальным набором:

  • Создать узел
  • Удалить узел
  • Подключить узлы

Все это легко на стороне модели, но очень сложно, если мы хотим создать хороший UX.

Создать узел легко, просто добавьте новый узел в наш массив узлов и заново сгенерируйте макет с помощью Dagre.

Чтобы удалить узлы, нам нужен способ выбрать узлы, а затем предоставить где-нибудь кнопку удаления (панель инструментов, сочетание клавиш и/или пункт во всплывающем меню).

Создание узла

В редакторе рабочего процесса RequestLogic создание привязано к существующим узлам. Вы можете создать либо дочерний узел, либо узел между двумя существующими узлами. По этой причине у нас есть много кнопок создания, которые уже указывают местоположение вновь созданного узла. Здесь, чтобы сделать его гибким и сохранить длину статьи ниже порога tl;dr, мы полагаемся на одну кнопку создания:

<button (click)="createNodeClicked()">CREATE</button>
---
createNodeClicked() {
  this.nodes.push(new Node(this.generateNodeId()));
  this.updateGraph();
}

Здесь метод updateGraph должен воссоздать наши массивы блоков и ребер с помощью Dagre так же, как мы делали это в предыдущей статье:

updateGraph() {
  var g = new dagre.graphlib.Graph();
  g.setGraph({});
  this.nodes.forEach(node => {
    g.setNode(node.id, { width: 200, height: 40 });
    node.relationIds.forEach(rel => {
      g.setEdge(rel, node.id, { node: node });
    });
  });
  dagre.layout(g);
  this.boxes = [];
  g.nodes().forEach(n => {
    var node = g.node(n);
    var box = new Box (n, node.x - node.width / 2, node.y -  node.height / 2, node.width, node.height, n);
    this.boxes.push(box);
  });
  this.edges = [];
  g.edges().forEach(e => {
    this.edges.push(new Edge(g.edge(e).points));
  });
}

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

generateNodeId(): string {
  const prefix = "Node ";
  var latestCode = "A";
  this.nodes.forEach(node => {
    if (node.id.startsWith(prefix)) {
      var code = node.id.substr(prefix.length, node.id.length -   prefix.length);
      if (code > latestCode) {
        latestCode = code;
      }
    }
  });
  if (latestCode.length == 1 && latestCode != 'Z') {
    latestCode = String.fromCharCode(latestCode.charCodeAt(0) + 1);
  } else {
    latestCode = "X" + Math.trunc(Math.random() * 100 / 100);
  }
  return prefix + latestCode;
}

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

Удаление узла

Нам нужен метод для удаления узла из нашего массива узлов:

removeNode(nodes: Node[], nodeId: string) {
  nodes.forEach(n => {
    var index = n.relationIds.indexOf(nodeId);
    if (index >= 0)
      n.relationIds.splice(index, 1);
  });
  var index = nodes.findIndex(n => n.id == nodeId);
  nodes.splice(index, 1);
}

Каким-то образом мы должны вызвать этот метод с правильным идентификатором узла. Давайте добавим поддержку выбора в наши коробки.

export class Box {
  public isSelected: boolean = false;
  constructor(
    public id: string,
    public x: number,
    public y: number,
    public width: number,
    public height: number,
    public label: string) {
  }
}

При нажатии на поле нам нужно установить для этого свойства isSelected значение true (или false, чтобы переключить его). Вот простая реализация для события щелчка коробки:

boxClicked(box: Box) {
  box.isSelected = !box.isSelected;
}

Отображение поля должно быть улучшено, чтобы вызвать этот метод и отображать поле по-другому, если оно выбрано:

<g *ngFor="let box of boxes" (click)="boxClicked(box)">
  <rect class="box" [ngClass]="{'selected': box.isSelected}" [attr.x]="box.x" [attr.y]="box.y" [attr.width]="box.width" [attr.height]="box.height" />
  <text [attr.x]="box.x + 10" [attr.y]="box.y + 24">{{ box.label }}</text>
</g>

Класс «выбранный» добавляется к прямоугольнику, если поле выбрано:

.box.selected {
  fill: #45449e88;
}

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

<button [disabled]="!hasSelectedNode" (click)="deleteNodeClicked()">DELETE</button>
---
deleteNodeClicked() {
  this.boxes.forEach(box => {
    if (box.isSelected) {
      this.removeNode(this.nodes, box.id);
    }
  });
  this.updateGraph();
}

Подключить узлы

Теперь у нас есть поддержка выбора узлов, поэтому мы можем улучшить и процесс создания. Что, если мы соединим вновь созданные узлы с выбранными узлами?

Чтобы добиться этого простого поведения, нам нужно обновить метод createNodeClicked:

getSelectedNodeIds(): string[] {
  return this.boxes.reduce((selectedNodeIds: string[], box: Box) => {
    if (box.isSelected) selectedNodeIds.push(box.id);
      return selectedNodeIds;
    }, []);
}
createNodeClicked() {
  var selectedNodeIds = this.getSelectedNodeIds();
  this.nodes.push(new Node(this.generateNodeId(), selectedNodeIds));
  this.updateGraph();
  this.boxes.forEach(box => {
    box.isSelected = selectedNodeIds.some(x => x == box.id);
  });
}

Резюме

Теперь мы прибили создание и удаление, но не закончили с подключением узлов. Края еще не направлены, однако наша модель имеет информацию о направлении. Кроме того, различные типы узлов, панорамирование, масштабирование еще даже не упоминаются…