Получил достаточно аплодисментов (на самом деле один) для первой части, так что, похоже, мне нужно раскрыть больше секретов создания 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); }); }
Резюме
Теперь мы прибили создание и удаление, но не закончили с подключением узлов. Края еще не направлены, однако наша модель имеет информацию о направлении. Кроме того, различные типы узлов, панорамирование, масштабирование еще даже не упоминаются…