From f3f24ecf3b06deee258d367a05571f29da0031ac Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Fri, 8 Aug 2025 17:37:52 -0300 Subject: [PATCH 01/11] feat: add methods to edit chart by adding, editing and removing nodes New API includes methods to add siblings, add children, add parent, update and remove nodes. Close #78 --- .../vaadin/addons/orgchart/OrgChart.java | 416 ++++++++++++++- .../orgchart/event/ChildrenAddedEvent.java | 69 +++ .../orgchart/event/NodeUpdatedEvent.java | 36 ++ .../orgchart/event/NodesRemovedEvent.java | 54 ++ .../orgchart/event/ParentAddedEvent.java | 54 ++ .../orgchart/event/SiblingsAddedEvent.java | 70 +++ .../META-INF/frontend/fc-orgchart.js | 119 ++++- .../vaadin/addons/orgchart/EditChartDemo.java | 483 ++++++++++++++++++ .../addons/orgchart/OrgchartDemoView.java | 1 + .../orgchart/edit-chart-demo-styles.css | 53 ++ 10 files changed, 1339 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ChildrenAddedEvent.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodeUpdatedEvent.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodesRemovedEvent.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ParentAddedEvent.java create mode 100644 src/main/java/com/flowingcode/vaadin/addons/orgchart/event/SiblingsAddedEvent.java create mode 100644 src/test/java/com/flowingcode/vaadin/addons/orgchart/EditChartDemo.java create mode 100644 src/test/resources/META-INF/resources/frontend/styles/orgchart/edit-chart-demo-styles.css diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index f1711df..612185a 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -23,6 +23,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.flowingcode.vaadin.addons.orgchart.client.OrgChartState; +import com.flowingcode.vaadin.addons.orgchart.event.ChildrenAddedEvent; +import com.flowingcode.vaadin.addons.orgchart.event.NodeUpdatedEvent; +import com.flowingcode.vaadin.addons.orgchart.event.NodesRemovedEvent; +import com.flowingcode.vaadin.addons.orgchart.event.ParentAddedEvent; +import com.flowingcode.vaadin.addons.orgchart.event.SiblingsAddedEvent; import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.ComponentEvent; @@ -33,14 +38,18 @@ import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.shared.Registration; +import elemental.json.JsonArray; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** * OrgChart component definition.
* Uses JQuery library OrgChart to show an organization chart.
- * More information about this library at https://github.com/dabeng/OrgChart + * More information about this library at + * https://github.com/dabeng/OrgChart * * @author pbartolo */ @@ -72,17 +81,13 @@ public OrgChart(OrgChartItem orgChartItem) { @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); - this.getElement() - .executeJs( - "this.initializeOrgChart($0,$1,$2)", - convertToJsonObj(state), - state.value, - this.getId().get()); + this.getElement().executeJs("this.initializeOrgChart($0,$1,$2)", convertToJsonObj(state), + state.value, this.getId().get()); } /** - * @deprecated This method is no longer needed. Initialization is done in {@link - * #onAttach(AttachEvent)}. + * @deprecated This method is no longer needed. Initialization is done in + * {@link #onAttach(AttachEvent)}. */ @Deprecated public void initializeChart() {} @@ -208,10 +213,8 @@ public void updateDraggedNode(String draggedNode, String dragZone, String dropZo OrgChartItem newParentItem = getById(Integer.valueOf(dropZone), orgChartItem); // update old parent (remove item) - List oldParentUpdated = - oldParentItem.getChildren().stream() - .filter(i -> !draggedNodeId.equals(i.getId())) - .collect(Collectors.toList()); + List oldParentUpdated = oldParentItem.getChildren().stream() + .filter(i -> !draggedNodeId.equals(i.getId())).collect(Collectors.toList()); oldParentItem.setChildren(oldParentUpdated); // update new parent (add item) @@ -242,7 +245,8 @@ private OrgChartItem getChildItemById(Integer id, List children) { * datasoure representing a node) and returns an HTML snippet. The name of this parameter is given * by {@code parameterName}. * - *

Example: + *

+ * Example: * setNodeTemplate("item","return ''+item.name+'';") * configures the following JS function as node template: * function(item) { return ''+item.name+''; } @@ -330,4 +334,386 @@ public OrgChart getOrgChart() { public void setCollapsedNodes() { setChartDepth(1); } + + /** + * Finds the parent of a node with the given ID in the org chart.
+ * This method is used to find the parent of a node after the chart has been initialized. + * + * @param childId the ID of the child node whose parent is to be found + * @param root the root of the org chart from which to start searching + * @return the parent {@link OrgChartItem} of the node with the given ID, or {@code null} if not + * found + */ + private OrgChartItem findParent(Integer childId, OrgChartItem root) { + if (root.getChildren() != null) { + for (OrgChartItem child : root.getChildren()) { + if (childId.equals(child.getId())) { + return root; + } + OrgChartItem parent = findParent(childId, child); + if (parent != null) { + return parent; + } + } + } + return null; + } + + /** + * Converts a JsonArray of IDs to a List of Integers. + * + * @param jsonIds array of numeric IDs + * @return list of converted integer IDs + */ + private List convertJsonArrayToIntegerList(JsonArray jsonIds) { + List idList = new ArrayList<>(); + for (int i = 0; i < jsonIds.length(); i++) { + idList.add((int) jsonIds.getNumber(i)); + } + return idList; + } + + /** + * Appends a list of items to a parent node's children list. * @param parentNode the node to which + * children will be added + * + * @param itemsToAdd the list of items to add + */ + private void appendItemsToParent(OrgChartItem parentNode, List itemsToAdd) { + if (parentNode != null) { + List currentChildren = parentNode.getChildren(); + if (currentChildren == null) { + currentChildren = new ArrayList<>(); + } + currentChildren.addAll(itemsToAdd); + parentNode.setChildren(currentChildren); + } + } + + /** + * Adds one or more sibling nodes to a target node in the chart. + *

+ * This method inserts the new items at the same level as the target node, under the same parent. + * It updates both the internal data structure and the client-side visual representation. + * + * @param nodeId the ID of the existing node that will serve as the anchor for adding siblings. + * This must not be the root node's ID. + * @param siblings a list of {@link OrgChartItem} objects to be added as siblings + * @throws IllegalArgumentException if the {@code nodeId} belongs to the root node of the chart, + * as the root cannot have siblings + */ + public void addSiblings(Integer nodeId, List siblings) { + // First check if selected node is not the root node + if (nodeId.equals(this.orgChartItem.getId())) { + throw new IllegalArgumentException("Cannot add siblings to the root node."); + } + // Update the internal data structure + OrgChartItem targetNode = getById(nodeId, orgChartItem); + if (targetNode != null) { + // Find parent of the target node + OrgChartItem parentNode = findParent(nodeId, orgChartItem); + if (parentNode != null) { + // Update parent's children list with the new siblings + appendItemsToParent(parentNode, siblings); + } + } + + // Update the visual representation by calling the client-side method addSiblings + String siblingsJson = convertToJsonObj(siblings); + this.getElement().executeJs("this.addSiblings($0, $1)", nodeId, siblingsJson); + } + + /** + * Handles sibling addition events from the client side. Converts the received JsonArray of + * sibling IDs to a List and fires a {@link SiblingsAddedEvent}. + * + * @param nodeId the ID of the node that received new siblings + * @param siblingIds array of IDs for the newly added siblings + */ + @ClientCallable + private void onSiblingsAdded(String nodeId, JsonArray siblingIds) { + // Find the node where siblings were added + OrgChartItem targetItem = getById(Integer.valueOf(nodeId), orgChartItem); + + // Convert the JsonArray to a simple list of integer IDs + List newSiblingIdList = convertJsonArrayToIntegerList(siblingIds); + + // Convert the list of IDs into a list of the actual OrgChartItem objects + List newSiblingItems = newSiblingIdList.stream() + .map(id -> getById(id, orgChartItem)).filter(Objects::nonNull).collect(Collectors.toList()); + + // Fire the event with the parent and the fully populated list of child items + fireSiblingsAddedEvent(targetItem, newSiblingItems, true); + } + + /** + * Adds a listener for sibling addition events. The listener will be notified when new siblings + * are added to any node in the chart. + * + * @param listener the listener to be added + * @return a {@link Registration} for removing the listener + */ + public void addSiblingsAddedListener(ComponentEventListener listener) { + addListener(SiblingsAddedEvent.class, listener); + } + + /** + * Fires a siblings added event. + * + * @param item the node that received new siblings + * @param newSibling list of the newly added siblings + * @param fromClient whether the event originated from the client side + */ + protected void fireSiblingsAddedEvent(OrgChartItem item, List newSiblings, + boolean fromClient) { + fireEvent(new SiblingsAddedEvent(this, item, newSiblings, fromClient)); + } + + /** + * Adds one or more child nodes to a specified parent node in the chart. + *

+ * This method updates both the internal data model and the client-side visuals. Note the specific + * client-side behavior: if the parent node has no existing children, this uses the library's + * {@code addChildren} function. If the parent already has children, it uses the + * {@code addSiblings} function on the first existing child to append the new nodes. + * + * @param nodeId the ID of the parent node to which the new children will be added + * @param children a list of {@link OrgChartItem} objects to be added as new children + */ + public void addChildren(Integer nodeId, List children) { + // Update the internal data structure + OrgChartItem targetNode = getById(nodeId, orgChartItem); + boolean currentChildrenEmpty = targetNode.getChildren().isEmpty(); + if (targetNode != null) { + // Add new children while preserving existing ones + appendItemsToParent(targetNode, children); + } + + // Update the visual representation + String itemsJson = convertToJsonObj(children); + if (currentChildrenEmpty) { + this.getElement().executeJs("this.addChildren($0, $1)", nodeId, itemsJson); + } else { + this.getElement().executeJs("this.addSiblings($0, $1)", + targetNode.getChildren().get(0).getId(), itemsJson); + } + } + + @ClientCallable + private void onChildrenAdded(String nodeId, JsonArray childIds) { + // Find the parent node where children were added + OrgChartItem parentItem = getById(Integer.valueOf(nodeId), orgChartItem); + + // Convert the JsonArray to a simple list of integer IDs + List newChildIdList = convertJsonArrayToIntegerList(childIds); + + // Convert the list of IDs into a list of the actual OrgChartItem objects + List newChildItems = newChildIdList.stream().map(id -> getById(id, orgChartItem)) + .filter(Objects::nonNull).collect(Collectors.toList()); + + // Fire the event with the parent and the fully populated list of child items + fireChildrenAddedEvent(parentItem, newChildItems, true); + } + + /** + * Adds a listener for child addition events. The listener will be notified when new children are + * added to any node in the chart. + * + * @param listener the listener to be added + * @return a {@link Registration} for removing the listener + */ + public Registration addChildrenAddedListener( + ComponentEventListener listener) { + return addListener(ChildrenAddedEvent.class, listener); + } + + /** + * Fires a children added event. + * + * @param item the node that received new children + * @param newChildren list of the newly added children + * @param fromClient whether the event originated from the client side + */ + protected void fireChildrenAddedEvent(OrgChartItem item, List newChildren, + boolean fromClient) { + fireEvent(new ChildrenAddedEvent(this, item, newChildren, fromClient)); + } + + /** + * Removes a specified node and all of its descendants from the chart. + *

+ * This action updates both the server-side data model and the client-side visualization. If the + * root node is removed, the chart will likely become empty. This operation is permanent for the + * current state of the chart. + * + * @param nodeId the ID of the node to remove. All children and subsequent descendants of this + * node will also be removed from the chart. + */ + public void removeNodes(Integer nodeId) { + // Find the node set for removal + OrgChartItem nodeToRemove = getById(nodeId, orgChartItem); + if (nodeToRemove != null) { + // Clear the removed node's children + nodeToRemove.setChildren(Collections.emptyList()); + // Find parent and remove node from its children + OrgChartItem parentNode = findParent(nodeId, orgChartItem); + if (parentNode != null) { + List currentChildren = parentNode.getChildren(); + currentChildren.removeIf(child -> nodeId.equals(child.getId())); + parentNode.setChildren(currentChildren); + } + + // Update the visual representation + this.getElement().executeJs("this.removeNodes($0)", nodeId); + } + } + + @ClientCallable + private void onNodesRemoved(String nodeId) { + fireNodesRemovedEvent(Integer.valueOf(nodeId), true); + } + + /** + * Adds a listener for node removal events. The listener will be notified when a node and its + * descendants are removed from the chart. + * + * @param listener the listener to be added + * @return a {@link Registration} for removing the listener + */ + public Registration addNodesRemovedListener(ComponentEventListener listener) { + return addListener(NodesRemovedEvent.class, listener); + } + + /** + * Fires a nodes removed event. + * + * @param nodeId the ID of the removed node + * @param fromClient whether the event originated from the client side + */ + protected void fireNodesRemovedEvent(Integer nodeId, boolean fromClient) { + fireEvent(new NodesRemovedEvent(this, nodeId, fromClient)); + } + + /** + * Adds a new parent node to the organization chart. This method: + *

    + *
  • Updates the visual representation of the chart
  • + *
  • Maintains the internal data structure by updating the root item
  • + *
+ * + * @param parentItem the new root item of the chart + */ + public void addParent(OrgChartItem newParentItem) { + // Update the internal data structure + if (this.orgChartItem != null) { + // Set the old root as the only child of the new parent node + newParentItem.setChildren(Collections.singletonList(this.orgChartItem)); + } + // Update the chart's root to point to the new parent + this.orgChartItem = newParentItem; + + // Update the visual representation by calling the client-side method addParent + String parentJson = convertToJsonObj(newParentItem); + this.getElement().executeJs("this.addParent($0)", parentJson); + } + + /** + * Handles parent addition events from the client side. The client sends the ID of the new + * parent/root node. + * + * @param newParentId the ID of the newly added parent node + */ + @ClientCallable + private void onParentAdded(String newParentId) { + OrgChartItem newParentItem = getById(Integer.valueOf(newParentId), orgChartItem); + if (newParentItem != null) { + fireParentAddedEvent(newParentItem, true); + } + } + + /** + * Adds a listener for parent addition event. The listener will be notified when a new parent + * (root) is added to the chart. + * + * @param listener the listener to be added + * @return a {@link Registration} for removing the listener + */ + public Registration addParentAddedListener(ComponentEventListener listener) { + return addListener(ParentAddedEvent.class, listener); + } + + /** + * Fires a parent added event. + * + * @param newParent the node that was added as the new parent/root + * @param fromClient whether the event originated from the client side + */ + protected void fireParentAddedEvent(OrgChartItem newParent, boolean fromClient) { + fireEvent(new ParentAddedEvent(this, newParent, fromClient)); + } + + /** + * Updates a node in the chart with new data. + *

+ * This method updates the server-side data model and then calls the client-side function to + * visually redraw the node with the new information. + * + * @param nodeId the ID of the node to update + * @param newDataItem an {@link OrgChartItem} containing the new data to be merged. The ID of this + * item is ignored; only its other properties (name, title, custom data, etc) are used for + * the update. + */ + public void updateNode(Integer nodeId, OrgChartItem newDataItem) { + OrgChartItem nodeToUpdate = getById(nodeId, this.orgChartItem); + if (nodeToUpdate != null) { + // Update the server-side object + if (newDataItem.getName() != null) { + nodeToUpdate.setName(newDataItem.getName()); + } + if (newDataItem.getTitle() != null) { + nodeToUpdate.setTitle(newDataItem.getTitle()); + } + if (newDataItem.getClassName() != null) { + nodeToUpdate.setClassName(newDataItem.getClassName()); + } + nodeToUpdate.setHybrid(newDataItem.isHybrid()); + + if (newDataItem.getData() != null) { + newDataItem.getData().forEach(nodeToUpdate::setData); + } + + // Call the client-side JS function to update the visual representation + String newDataJson = convertToJsonObj(newDataItem); + this.getElement().executeJs("this.updateNode($0, $1)", nodeId.toString(), newDataJson); + } + } + + @ClientCallable + private void onNodeUpdated(String nodeId) { + OrgChartItem updatedItem = getById(Integer.valueOf(nodeId), orgChartItem); + if (updatedItem != null) { + fireNodeUpdatedEvent(updatedItem, true); + } + } + + /** + * Adds a listener for node updated event. + * + * @param listener the listener to be added + * @return a {@link Registration} for removing the listener + */ + public Registration addNodeUpdatedListener(ComponentEventListener listener) { + return addListener(NodeUpdatedEvent.class, listener); + } + + /** + * Fires a node updated event. + * + * @param item the updated node + * @param fromClient whether the event originated from the client side + */ + protected void fireNodeUpdatedEvent(OrgChartItem item, boolean fromClient) { + fireEvent(new NodeUpdatedEvent(this, item, fromClient)); + } + } diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ChildrenAddedEvent.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ChildrenAddedEvent.java new file mode 100644 index 0000000..8c7db8f --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ChildrenAddedEvent.java @@ -0,0 +1,69 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.orgchart.event; + +import com.flowingcode.vaadin.addons.orgchart.OrgChart; +import com.flowingcode.vaadin.addons.orgchart.OrgChartItem; +import com.vaadin.flow.component.ComponentEvent; +import java.util.ArrayList; +import java.util.List; + +/** + * Event fired when children are added to a node in the organization chart. Contains information + * about both the parent node and the newly added children. + */ +@SuppressWarnings("serial") +public class ChildrenAddedEvent extends ComponentEvent { + private final OrgChartItem item; + private final List newChildren; + + /** + * Creates a new children added event. + * + * @param source the chart component that fired the event + * @param item the node that received new children + * @param newChildren list of the newly added children + * @param fromClient whether the event originated from the client side + */ + public ChildrenAddedEvent(OrgChart source, OrgChartItem item, List newChildren, + boolean fromClient) { + super(source, fromClient); + this.item = item; + this.newChildren = new ArrayList<>(newChildren); + } + + /** + * Gets the node that received new children. + * + * @return the node + */ + public OrgChartItem getItem() { + return item; + } + + /** + * Gets the list of the newly added children. + * + * @return the list of new children + */ + public List getNewChildren() { + return newChildren; + } +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodeUpdatedEvent.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodeUpdatedEvent.java new file mode 100644 index 0000000..5f2982f --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodeUpdatedEvent.java @@ -0,0 +1,36 @@ +package com.flowingcode.vaadin.addons.orgchart.event; + +import com.flowingcode.vaadin.addons.orgchart.OrgChart; +import com.flowingcode.vaadin.addons.orgchart.OrgChartItem; +import com.vaadin.flow.component.ComponentEvent; + + +/** + * Event thrown when a node is updated. + */ +@SuppressWarnings("serial") +public class NodeUpdatedEvent extends ComponentEvent { + private final OrgChartItem updatedItem; + + /** + * Creates a node updated event. + * + * @param source the chart component that fired the event + * @param updatedItem the node being updated + * @param fromClient whether the event originated from the client side + */ + public NodeUpdatedEvent(OrgChart source, OrgChartItem updatedItem, boolean fromClient) { + super(source, fromClient); + this.updatedItem = updatedItem; + } + + /** + * Gets the updated node. + * + * @return the updated node item + */ + public OrgChartItem getUpdatedItem() { + return updatedItem; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodesRemovedEvent.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodesRemovedEvent.java new file mode 100644 index 0000000..05f552e --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/NodesRemovedEvent.java @@ -0,0 +1,54 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.orgchart.event; + +import com.flowingcode.vaadin.addons.orgchart.OrgChart; +import com.vaadin.flow.component.ComponentEvent; + +/** + * Event fired when a node and its descendants are removed from the organization chart. Contains + * information about the removed node. + */ +@SuppressWarnings("serial") +public class NodesRemovedEvent extends ComponentEvent { + private final Integer nodeId; + + /** + * Creates a new nodes removed event. + * + * @param source the chart component that fired the event + * @param nodeId the ID of the removed node + * @param fromClient whether the event originated from the client side + */ + public NodesRemovedEvent(OrgChart source, Integer nodeId, boolean fromClient) { + super(source, fromClient); + this.nodeId = nodeId; + } + + /** + * Gets the ID of the removed node. + * + * @return the node ID + */ + public Integer getNodeId() { + return nodeId; + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ParentAddedEvent.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ParentAddedEvent.java new file mode 100644 index 0000000..33856eb --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/ParentAddedEvent.java @@ -0,0 +1,54 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.orgchart.event; + +import com.flowingcode.vaadin.addons.orgchart.OrgChart; +import com.flowingcode.vaadin.addons.orgchart.OrgChartItem; +import com.vaadin.flow.component.ComponentEvent; + +/** + * Event fired when a new parent is added to the chart. + */ +@SuppressWarnings("serial") +public class ParentAddedEvent extends ComponentEvent { + + private final OrgChartItem newParent; + + /** + * Creates a new event. + * + * @param source the component that fired the event + * @param newParent the item that was added as the new parent + * @param fromClient true if the event originated from the client + */ + public ParentAddedEvent(OrgChart source, OrgChartItem newParent, boolean fromClient) { + super(source, fromClient); + this.newParent = newParent; + } + + /** + * Gets the item that was added as the new parent/root. + * + * @return the new parent item + */ + public OrgChartItem getNewParent() { + return newParent; + } +} \ No newline at end of file diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/SiblingsAddedEvent.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/SiblingsAddedEvent.java new file mode 100644 index 0000000..e2e6fc3 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/event/SiblingsAddedEvent.java @@ -0,0 +1,70 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.orgchart.event; + +import java.util.ArrayList; +import java.util.List; + +import com.flowingcode.vaadin.addons.orgchart.OrgChart; +import com.flowingcode.vaadin.addons.orgchart.OrgChartItem; +import com.vaadin.flow.component.ComponentEvent; + +/** + * Event fired when siblings are added to a node in the organization chart. Contains information + * about both the target node and the newly added siblings. + */ +@SuppressWarnings("serial") +public class SiblingsAddedEvent extends ComponentEvent { + private final OrgChartItem item; + private final List newSiblings; + + /** + * Creates a new siblings added event. + * + * @param source the chart component that fired the event + * @param item the node that received new siblings + * @param newSiblings list of the newly added siblings + * @param fromClient whether the event originated from the client side + */ + public SiblingsAddedEvent(OrgChart source, OrgChartItem item, List newSiblings, + boolean fromClient) { + super(source, fromClient); + this.item = item; + this.newSiblings = new ArrayList<>(newSiblings); + } + + /** + * Gets the node that received new siblings. + * + * @return the node + */ + public OrgChartItem getItem() { + return item; + } + + /** + * Gets the list of the newly added siblings. + * + * @return the list of new sibling + */ + public List getNewSiblings() { + return newSiblings; + } +} diff --git a/src/main/resources/META-INF/frontend/fc-orgchart.js b/src/main/resources/META-INF/frontend/fc-orgchart.js index 6071b4e..38f8efa 100644 --- a/src/main/resources/META-INF/frontend/fc-orgchart.js +++ b/src/main/resources/META-INF/frontend/fc-orgchart.js @@ -146,7 +146,124 @@ class FCOrgChart extends PolymerElement { }); } + this._chartInstance = orgchart; } + + /** + * Adds sibling nodes for designated node. + * + * @param nodeId the node id to add new siblings to + * @param siblings the new sibling data + * @see {@link https://github.com/dabeng/OrgChart/tree/v3.7.0?tab=readme-ov-file#addsiblingsnode-data|OrgChart Documentation addSiblings($node, data)} + */ + addSiblings(nodeId, siblings) { + var $ = window.jQuery || jQuery; + const $node = $('#' + nodeId); + if ($node.length) { + const siblingsData = typeof siblings === 'string' ? JSON.parse(siblings) : siblings; + if ($node.length && this._chartInstance) { + try { + this._chartInstance.addSiblings($node, siblingsData); + } catch (error) { + // This prevents validation error from stopping the script + } + + // Notify server about siblings added with just the IDs + const siblingIds = siblingsData.map(sibling => sibling.id); + this.$server.onSiblingsAdded(nodeId, siblingIds); + } + } + } + + /** + * Adds child nodes for designed node. + * + * @param nodeId the node id to add new children to + * @param children the new children data + * @see {@link https://github.com/dabeng/OrgChart/tree/v3.7.0?tab=readme-ov-file#addchildrennode-data|OrgChart Documentation addChildren($node, data)} + */ + addChildren(nodeId, children) { + var $ = window.jQuery || jQuery; + const $node = $('#' + nodeId); + if ($node.length) { + const childrenData = typeof children === 'string' ? JSON.parse(children) : children; + if ($node.length && this._chartInstance) { + this._chartInstance.addChildren($node, childrenData); + // Notify server about children added with just the IDs + const childIds = childrenData.map(child => child.id); + this.$server.onChildrenAdded(nodeId, childIds); + } + } + } + + /** + * Removes the designated node and its descedant nodes. + * + * @param nodeId the node id to be removed + * @see {@link https://github.com/dabeng/OrgChart/tree/v3.7.0?tab=readme-ov-file#removenodesnode|OrgChart Documentation removeNodes($node)} + */ + removeNodes(nodeId) { + var $ = window.jQuery || jQuery; + const $node = $('#' + nodeId); + if ($node.length && this._chartInstance) { + this._chartInstance.removeNodes($node); + this.$server.onNodesRemoved(nodeId); + } + } + + /** + * Adds a new parent to the chart. + * + * @param parent the new parent node to be added + * @see {@link https://github.com/dabeng/OrgChart/tree/v3.7.0?tab=readme-ov-file#addparentdata|OrgChart Documentation addParent(data)} + */ + addParent(parent) { + const parentData = typeof parent === 'string' ? JSON.parse(parent) : parent; + if (this._chartInstance) { + // Find the current root node element in the chart + const $currentRoot = this._chartInstance.$chart.find('.node:first'); + if ($currentRoot.length) { + this._chartInstance.addParent($currentRoot, parentData); + this.$server.onParentAdded(parentData.id); + } else { + console.error('OrgChart: Could not find the current root node to attach a parent to.'); + } + } + } + + /** + * Updates a node with new data and redraws the chart. + * + * @param nodeId the ID of the node to update + * @param newData an object or JSON string containing the new data for the node + */ + updateNode(nodeId, newData) { + if (!this._chartInstance) { + return; + } + + // Get the current full data hierarchy from the chart instance + const hierarchy = this._chartInstance.getHierarchy(true); + + // Use JSONDigger to find the node to be updated + const digger = new window.JSONDigger(hierarchy, this._chartInstance.options.nodeId, 'children'); + const nodeToUpdate = digger.findNodeById(parseInt(nodeId, 10)); + + if (nodeToUpdate) { + // Parse the new data and merge it into the found node + const dataToMerge = typeof newData === 'string' ? JSON.parse(newData) : newData; + // Delete the ID from the new data to prevent it from being overwritten + delete dataToMerge.id; + // Merge the data + Object.assign(nodeToUpdate, dataToMerge); + + // Re-initialize the chart with the updated hierarchy + // This redraws the chart to reflect the changes + this._chartInstance.init({ 'data': hierarchy }); + // Notify server about the node update + this.$server.onNodeUpdated(nodeId); + } + } isIEBrowser() { var sAgent = window.navigator.userAgent; @@ -182,7 +299,7 @@ class FCOrgChart extends PolymerElement { static get properties() { return { - // Declare your properties here. + _chartInstance: Object }; } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/orgchart/EditChartDemo.java b/src/test/java/com/flowingcode/vaadin/addons/orgchart/EditChartDemo.java new file mode 100644 index 0000000..411a89c --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/orgchart/EditChartDemo.java @@ -0,0 +1,483 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.orgchart; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; +import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.radiobutton.RadioButtonGroup; +import com.vaadin.flow.component.radiobutton.RadioGroupVariant; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * Demo view to demonstrate how an {@code OrgChart} can be edited. + *

+ * This view showcases the following operations: + *

    + *
  • Adding siblings to an existing node.
  • + *
  • Adding children to an existing node.
  • + *
  • Adding a new parent (root) to the entire chart.
  • + *
  • Removing nodes from the chart.
  • + *
  • Updating the data of a selected node.
  • + *
+ */ +@SuppressWarnings("serial") +@PageTitle("Edit Chart") +@DemoSource +@Route(value = "orgchart/edit-chart", layout = OrgchartDemoView.class) +@CssImport("./styles/orgchart/edit-chart-demo-styles.css") +public class EditChartDemo extends VerticalLayout { + + private static final AtomicInteger idCounter = new AtomicInteger(100); + + private OrgChart orgChart; + private OrgChartItem selectedNode; + private TextField selectedNodeNameField; + private VerticalLayout newNodeFieldsLayout; + private HorizontalLayout newNodeButtonsLayout; + private RadioButtonGroup typeSelector; + private Button addButton; + private Button deleteButton; + private Button updateButton; + private Button resetButton; + private HorizontalLayout newNodeActionsColumn; + private RadioButtonGroup actionSelector; + + public EditChartDemo() { + setSizeFull(); + initChart(); + add(orgChart, getEditionLayout()); + } + + private void initChart() { + orgChart = getChart(); + orgChart.addClassNames("chart-container", "editable-chart"); + orgChart.setChartTitle( + "EDIT CHART - Add Children, Add Siblings, Add Parent, Remove Nodes, Edit Selected Node"); + orgChart.setChartNodeContent("title"); + + // Add listener for node click + orgChart.addOnNodeClickListener(e -> { + selectedNode = e.getClickedItem(); + selectedNodeNameField.setValue(selectedNode.getName()); + updateComponentStates(); + }); + + // Add listener to know when siblings are added + orgChart.addSiblingsAddedListener(e -> { + OrgChartItem targetNode = e.getItem(); + List newSiblings = e.getNewSiblings(); + if (!newSiblings.isEmpty()) { + String siblingNames = + newSiblings.stream().map(OrgChartItem::getName).collect(Collectors.joining(", ")); + String message = + String.format("New siblings \"%s\" added to %s", siblingNames, targetNode.getName()); + Notification.show(message); + // Console - Show hierarchy updated after adding siblings + System.out.println( + "------ OrgChart updated: ------\n" + e.getSource().getOrgChartItem().toString()); + } + }); + + // Add listener to know when children are added + orgChart.addChildrenAddedListener(e -> { + OrgChartItem targetNode = e.getItem(); + List newChildren = e.getNewChildren(); + String childrenNames = + newChildren.stream().map(OrgChartItem::getName).collect(Collectors.joining(", ")); + String message = + String.format("New children \"%s\" added to %s", childrenNames, targetNode.getName()); + Notification.show(message); + // Console - Show hierarchy updated after adding children + System.out.println( + "------ OrgChart updated: ------\n" + e.getSource().getOrgChartItem().toString()); + }); + + // Add listener on nodes removal + orgChart.addNodesRemovedListener(e -> { + String message = String.format("Item with id %s (and its descedant nodes) removed from chart", + e.getNodeId()); + Notification.show(message); + // Console - Show hierarchy updated after removing nodes + System.out.println( + "------ OrgChart updated: ------\n" + e.getSource().getOrgChartItem().toString()); + }); + + // Add listener on new parent added + orgChart.addParentAddedListener(e -> { + String message = + String.format("New parent \"%s\" added to chart", e.getNewParent().getName()); + Notification.show(message); + // Console - Show hierarchy updated after adding a new parent + System.out.println( + "------ OrgChart updated: ------\n" + e.getSource().getOrgChartItem().toString()); + }); + + // Add listener when a node data is updated + orgChart.addNodeUpdatedListener(e -> { + String message = String.format("Node \"%s\" was updated.", e.getUpdatedItem().getName()); + Notification.show(message); + // Console - Show hierarchy updated after editing a node + System.out.println( + "------ OrgChart updated: ------\n" + e.getSource().getOrgChartItem().toString()); + }); + } + + private OrgChart getChart() { + OrgChartItem item1 = new OrgChartItem(1, "John Williams", "Director"); + OrgChartItem item2 = new OrgChartItem(2, "Anna Thompson", "Administration"); + OrgChartItem item3 = new OrgChartItem(3, "Timothy Jones", "Sub-Director"); + item1.setChildren(Arrays.asList(item2, item3)); + OrgChartItem item4 = new OrgChartItem(4, "Louise Night", "Department 1"); + OrgChartItem item5 = new OrgChartItem(5, "John Porter", "Department 2"); + item2.setChildren(Arrays.asList(item4, item5)); + OrgChartItem item6 = new OrgChartItem(6, "Charles Thomas", "Department 3"); + item5.setChildren(Arrays.asList(item6)); + return new OrgChart(item1); + } + + private VerticalLayout getEditionLayout() { + // Main container for the entire panel + VerticalLayout editionPanel = new VerticalLayout(); + editionPanel.addClassName("edition-panel"); + editionPanel.setSpacing(false); + + // Action Selector + actionSelector = new RadioButtonGroup<>(); + actionSelector.setLabel("Select Action"); + actionSelector.setItems("Add", "Edit", "Delete"); + actionSelector.addValueChangeListener(event -> updateComponentStates()); + + Div separator = new Div(); + separator.addClassName("edition-panel-separator"); + separator.setWidthFull(); + + // Main Controls Layout + HorizontalLayout mainControlsLayout = new HorizontalLayout(); + mainControlsLayout.setWidthFull(); + mainControlsLayout.addClassName("main-controls-layout"); + mainControlsLayout.setAlignItems(Alignment.BASELINE); + + // Selected node layout (selected node and relation type selector) + VerticalLayout selectedNodeColumn = createVerticalLayout(); + selectedNodeColumn.addClassName("selected-node-layout"); + selectedNodeColumn.setAlignItems(Alignment.START); + + // Selected node name text field + selectedNodeNameField = new TextField("Selected Node:"); + selectedNodeNameField.setPlaceholder("Node Name"); + selectedNodeNameField.setWidth("180px"); + selectedNodeNameField.setReadOnly(true); + + // Relation type selector + typeSelector = new RadioButtonGroup(); + typeSelector.setLabel("Select Relation Type:"); + typeSelector.setItems("Parent(root)", "Child", "Sibling"); + typeSelector.setValue("Child"); + typeSelector.addThemeVariants(RadioGroupVariant.LUMO_VERTICAL); + + // New node(s) layout (dynamic) + newNodeFieldsLayout = createVerticalLayout(); + newNodeFieldsLayout.addClassName("new-nodes-layout"); + + // Action buttons + addButton = new Button("Add Child to Selected Node"); + deleteButton = new Button("Delete Selected Node"); + updateButton = new Button("Edit Selected Node"); + resetButton = new Button("Reset Chart"); + + // Add value-change listener to relation type selector + typeSelector.addValueChangeListener(event -> { + switch (event.getValue()) { + case "Parent(root)": + addButton.setText("Add Parent Node"); + break; + case "Sibling": + addButton.setText("Add Sibling to Selected Node"); + break; + default: + addButton.setText("Add Child to Selected Node"); + break; + } + updateComponentStates(); + }); + + selectedNodeColumn.add(selectedNodeNameField, typeSelector); + + // Add the first text field for a new node initially + TextField initialNodeField = createNewNodeTextField(); + initialNodeField.setLabel("Add New Node Name:"); + newNodeFieldsLayout.add(initialNodeField); + + // Create add/remove buttons for new nodes + // These buttons will be used to add or remove text fields for new nodes + Button addButtonSmall = createIconButton(VaadinIcon.PLUS); + Button removeButtonSmall = createIconButton(VaadinIcon.MINUS); + + newNodeButtonsLayout = new HorizontalLayout(addButtonSmall, removeButtonSmall); + newNodeButtonsLayout.addClassName("new-nodes-buttons-layout"); + newNodeButtonsLayout.setPadding(false); + newNodeButtonsLayout.setSpacing(false); + + // Click listener for the '+' button to add a new text field + addButtonSmall.addClickListener(e -> newNodeFieldsLayout.add(createNewNodeTextField())); + + // Click listener for the '-' button to remove the last added text field + removeButtonSmall.addClickListener(e -> { + if (newNodeFieldsLayout.getComponentCount() > 1) { + Component lastField = + newNodeFieldsLayout.getComponentAt(newNodeFieldsLayout.getComponentCount() - 1); + newNodeFieldsLayout.remove(lastField); + } + }); + + // Adding new nodes layout + newNodeActionsColumn = new HorizontalLayout(newNodeFieldsLayout, newNodeButtonsLayout); + newNodeActionsColumn.setAlignItems(Alignment.BASELINE); + + // Add listeners to actions + addButton.addClickListener(e -> onAddButtonClick()); + deleteButton.addClickListener(e -> onDeleteButtonClick()); + updateButton.addClickListener(e -> onUpdateButtonClick()); + resetButton.addClickListener(e -> onResetButtonClick()); + + // Layout for action buttons + VerticalLayout actionButtonsColumn = createVerticalLayout(); + actionButtonsColumn.add(addButton, deleteButton, updateButton, resetButton); + actionButtonsColumn.setJustifyContentMode(JustifyContentMode.END); + actionButtonsColumn.setAlignItems(Alignment.END); + + // Add all columns to the main controls layout + mainControlsLayout.add(selectedNodeColumn, newNodeActionsColumn, actionButtonsColumn); + + // Add the actionSelector and the main controls to the final panel + editionPanel.add(actionSelector, separator, mainControlsLayout); + + // Set the initial state + updateComponentStates(); + + return editionPanel; + } + + private void updateComponentStates() { + String action = actionSelector.getValue(); + boolean isAdd = "Add".equals(action); + boolean isEdit = "Edit".equals(action); + boolean isDelete = "Delete".equals(action); + boolean nodeIsSelected = (selectedNode != null); + + String relation = typeSelector.getValue(); + boolean isParentMode = "Parent(root)".equals(relation); + + if(isParentMode) { + resetNewNodeFields(); + } + + typeSelector.setVisible(isAdd); + newNodeActionsColumn.setVisible(isAdd); + newNodeButtonsLayout.setVisible(isAdd && !isParentMode); + selectedNodeNameField.setVisible(isEdit || isDelete || (isAdd && !isParentMode)); + + addButton.setEnabled(isAdd); + updateButton.setEnabled(isEdit && nodeIsSelected); + deleteButton.setEnabled(isDelete && nodeIsSelected); + } + + private void onAddButtonClick() { + // Make sure a node is selected + if (selectedNode == null && selectedNodeNameField.isVisible()) { + Notification.show("Please select a node first."); + return; + } + + // Get all non-empty names from the text fields + List newNodeNames = + newNodeFieldsLayout.getChildren().filter(component -> component instanceof TextField) + .map(component -> ((TextField) component).getValue()) + .filter(name -> name != null && !name.trim().isEmpty()).collect(Collectors.toList()); + + if (newNodeNames.isEmpty()) { + Notification.show("Please enter a name for the new node(s)."); + return; + } + + // Create new OrgChartItem objects with unique IDs + List newItems = new ArrayList<>(); + for (String name : newNodeNames) { + int newId = idCounter.getAndIncrement(); + OrgChartItem newItem = new OrgChartItem(newId, name, "Undefined"); + newItems.add(newItem); + } + + // Add the new nodes to the chart + String relationType = typeSelector.getValue(); + + switch (relationType) { + case "Child": + orgChart.addChildren(selectedNode.getId(), newItems); + break; + case "Sibling": + try { + orgChart.addSiblings(selectedNode.getId(), newItems); + } catch (IllegalArgumentException ex) { + Notification.show(ex.getMessage()); + } + break; + case "Parent(root)": + orgChart.addParent(newItems.get(0)); + break; + } + + // Reset the text fields for the next operation + resetNewNodeFields(); + // Clear the text from the first field + ((TextField) newNodeFieldsLayout.getComponentAt(0)).clear(); + } + + private void onDeleteButtonClick() { + if (selectedNode == null) { + Notification.show("Please select a node to delete."); + return; + } + + Integer selectedNodeId = selectedNode.getId(); + // Check if the selected node is the root + boolean isRootNode = selectedNodeId.equals(orgChart.getOrgChartItem().getId()); + + if (isRootNode) { + // If it is the root, create and show a confirmation dialog + ConfirmDialog dialog = new ConfirmDialog(); + dialog.setHeader("Delete Entire Chart?"); + dialog.setText( + "Are you sure you want to delete the root node? This action will remove the entire chart."); + + dialog.setConfirmText("Delete Chart"); + dialog.setConfirmButtonTheme("error primary"); + dialog.setCancelable(true); + + dialog.addConfirmListener(event -> { + orgChart.removeNodes(selectedNodeId); + clearSelectedNode(); + Notification.show("Chart deleted."); + }); + + dialog.open(); + } else { + orgChart.removeNodes(selectedNodeId); + clearSelectedNode(); + Notification.show("Node deleted."); + } + } + + private void onUpdateButtonClick() { + if (selectedNode == null) { + Notification.show("Please select a node to update."); + return; + } + + // Create a dialog for editing + ConfirmDialog dialog = new ConfirmDialog(); + dialog.setHeader("Edit Node Details"); + + // Create fields for name and title, pre-filled with current data + TextField nameField = new TextField("Name"); + nameField.setValue(selectedNode.getName()); + nameField.setWidthFull(); + + TextField titleField = new TextField("Title"); + titleField.setValue(selectedNode.getTitle()); + titleField.setWidthFull(); + + dialog.add(new VerticalLayout(nameField, titleField)); + dialog.setConfirmText("Save"); + dialog.setConfirmButtonTheme("primary"); + dialog.setCancelable(true); + + // Add a listener for the confirm (Save) button + dialog.addConfirmListener(event -> { + // Create a new item with the updated data (using a temporary ID) + OrgChartItem updatedData = new OrgChartItem(0, nameField.getValue(), titleField.getValue()); + + // Call the updateNode method + orgChart.updateNode(selectedNode.getId(), updatedData); + }); + + dialog.open(); + } + + private void onResetButtonClick() { + OrgChart oldChart = this.orgChart; + initChart(); + this.replace(oldChart, this.orgChart); + clearSelectedNode(); + actionSelector.setValue(null); + } + + private void clearSelectedNode() { + selectedNode = null; + selectedNodeNameField.clear(); + updateComponentStates(); + } + + private Button createIconButton(VaadinIcon icon) { + Button iconButton = new Button(icon.create()); + iconButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_SMALL, + ButtonVariant.LUMO_TERTIARY_INLINE); + return iconButton; + } + + private TextField createNewNodeTextField() { + TextField newNodeTextField = new TextField(); + newNodeTextField.setPlaceholder("Name"); + newNodeTextField.setWidth("150px"); + return newNodeTextField; + } + + private VerticalLayout createVerticalLayout() { + VerticalLayout verticalLayout = new VerticalLayout(); + verticalLayout.setWidth("auto"); + verticalLayout.setSpacing(false); + verticalLayout.setPadding(false); + return verticalLayout; + } + + private void resetNewNodeFields() { + // Reset the text fields for the next operation + while (newNodeFieldsLayout.getComponentCount() > 1) { + // Remove all fields except the first one + newNodeFieldsLayout.remove(newNodeFieldsLayout.getComponentAt(1)); + } + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/orgchart/OrgchartDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/orgchart/OrgchartDemoView.java index f714811..d1bb147 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/orgchart/OrgchartDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/orgchart/OrgchartDemoView.java @@ -41,5 +41,6 @@ public OrgchartDemoView() { addDemo(ImageInTitleDemo.class); addDemo(HybridEnhancedChartDemo.class); addDemo(HybridDataPropertyDemo.class); + addDemo(EditChartDemo.class); } } diff --git a/src/test/resources/META-INF/resources/frontend/styles/orgchart/edit-chart-demo-styles.css b/src/test/resources/META-INF/resources/frontend/styles/orgchart/edit-chart-demo-styles.css new file mode 100644 index 0000000..9f21b17 --- /dev/null +++ b/src/test/resources/META-INF/resources/frontend/styles/orgchart/edit-chart-demo-styles.css @@ -0,0 +1,53 @@ +/*- + * #%L + * OrgChart Add-on + * %% + * Copyright (C) 2017 - 2025 Flowing Code S.A. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +.edition-panel { + border: 1px solid var(--lumo-contrast-10pct); + border-radius: 8px; + padding-top: 0; +} + +.edition-panel-separator { + border-top: 1px solid var(--lumo-contrast-10pct); + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.new-nodes-layout { + padding-bottom: 8px; +} + +.new-nodes-buttons-layout { + margin-left: 5px; +} + +.selected-node-layout { + margin-left: 0; +} + +.main-controls-layout { + flex-wrap: wrap; + gap: 1em; +} + +/* This targets the direct children inside the layout. */ +.main-controls-layout > * { + flex: 1 1 0; + min-width: 200px; +} \ No newline at end of file From b06627ac3e240dab0f0f4a5de95fc48476d5eca0 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 15:15:07 -0300 Subject: [PATCH 02/11] build: update version to 5.3.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 411cf4b..81bfd62 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.flowingcode.vaadin.addons orgchart-addon - 5.2.1-SNAPSHOT + 5.3.0-SNAPSHOT OrgChart Add-on From 2a683b5be10f07fec9ff96e0ed8cf935c5f29992 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 15:53:02 -0300 Subject: [PATCH 03/11] WIP: fix node update logic to prevent children removal --- src/main/resources/META-INF/frontend/fc-orgchart.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/META-INF/frontend/fc-orgchart.js b/src/main/resources/META-INF/frontend/fc-orgchart.js index 38f8efa..9ddf115 100644 --- a/src/main/resources/META-INF/frontend/fc-orgchart.js +++ b/src/main/resources/META-INF/frontend/fc-orgchart.js @@ -254,6 +254,8 @@ class FCOrgChart extends PolymerElement { const dataToMerge = typeof newData === 'string' ? JSON.parse(newData) : newData; // Delete the ID from the new data to prevent it from being overwritten delete dataToMerge.id; + // Avoid children list to be overwritten, the node should keep it's original children list + delete dataToMerge.children; // Merge the data Object.assign(nodeToUpdate, dataToMerge); From 075c10868e0d97e67696218e53649338b512edac Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 15:54:59 -0300 Subject: [PATCH 04/11] WIP: fix javadoc formatting --- .../java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index 612185a..f9ea5b4 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -374,9 +374,9 @@ private List convertJsonArrayToIntegerList(JsonArray jsonIds) { } /** - * Appends a list of items to a parent node's children list. * @param parentNode the node to which - * children will be added + * Appends a list of items to a parent node's children list. * + * @param parentNode the node to which children will be added * @param itemsToAdd the list of items to add */ private void appendItemsToParent(OrgChartItem parentNode, List itemsToAdd) { From d7f7484522db603529f87b85cd543aeb2f833f87 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 15:57:44 -0300 Subject: [PATCH 05/11] WIP: fix missing return type in listener registration --- .../java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index f9ea5b4..abecef9 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -453,8 +453,8 @@ private void onSiblingsAdded(String nodeId, JsonArray siblingIds) { * @param listener the listener to be added * @return a {@link Registration} for removing the listener */ - public void addSiblingsAddedListener(ComponentEventListener listener) { - addListener(SiblingsAddedEvent.class, listener); + public Registration addSiblingsAddedListener(ComponentEventListener listener) { + return addListener(SiblingsAddedEvent.class, listener); } /** From 77c3bc875aeffd5a606f6b2adb6ecf2f3da726e4 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 16:04:27 -0300 Subject: [PATCH 06/11] WIP: add missing null check --- .../java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index abecef9..90df2a3 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -483,7 +483,8 @@ protected void fireSiblingsAddedEvent(OrgChartItem item, List newS public void addChildren(Integer nodeId, List children) { // Update the internal data structure OrgChartItem targetNode = getById(nodeId, orgChartItem); - boolean currentChildrenEmpty = targetNode.getChildren().isEmpty(); + boolean currentChildrenEmpty = + targetNode.getChildren() == null || targetNode.getChildren().isEmpty(); if (targetNode != null) { // Add new children while preserving existing ones appendItemsToParent(targetNode, children); From d63b6e58374df43e790da32a51da5c27aa2139bc Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 16:18:12 -0300 Subject: [PATCH 07/11] WIP: improve updateNode logic --- .../com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index 90df2a3..a2d2c20 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -677,15 +677,16 @@ public void updateNode(Integer nodeId, OrgChartItem newDataItem) { if (newDataItem.getClassName() != null) { nodeToUpdate.setClassName(newDataItem.getClassName()); } - nodeToUpdate.setHybrid(newDataItem.isHybrid()); - + if (nodeToUpdate.isHybrid() != newDataItem.isHybrid()) { + nodeToUpdate.setHybrid(newDataItem.isHybrid()); + } if (newDataItem.getData() != null) { newDataItem.getData().forEach(nodeToUpdate::setData); } // Call the client-side JS function to update the visual representation String newDataJson = convertToJsonObj(newDataItem); - this.getElement().executeJs("this.updateNode($0, $1)", nodeId.toString(), newDataJson); + this.getElement().executeJs("this.updateNode($0, $1)", nodeId, newDataJson); } } From 7cb2a820c70be5e1466db9b64bea646bb9e0c5ea Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 16:30:09 -0300 Subject: [PATCH 08/11] WIP: fix javadoc --- .../java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index a2d2c20..becd02a 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -602,7 +602,7 @@ protected void fireNodesRemovedEvent(Integer nodeId, boolean fromClient) { *
  • Maintains the internal data structure by updating the root item
  • * * - * @param parentItem the new root item of the chart + * @param newParentItem the new root item of the chart */ public void addParent(OrgChartItem newParentItem) { // Update the internal data structure From d183f415348fc6674cc9a5b56952f34e890cc62d Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 16:35:17 -0300 Subject: [PATCH 09/11] WIP: add missing null checks --- .../vaadin/addons/orgchart/OrgChart.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index becd02a..c996e43 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -434,16 +434,19 @@ public void addSiblings(Integer nodeId, List siblings) { private void onSiblingsAdded(String nodeId, JsonArray siblingIds) { // Find the node where siblings were added OrgChartItem targetItem = getById(Integer.valueOf(nodeId), orgChartItem); + if (targetItem != null) { - // Convert the JsonArray to a simple list of integer IDs - List newSiblingIdList = convertJsonArrayToIntegerList(siblingIds); + // Convert the JsonArray to a simple list of integer IDs + List newSiblingIdList = convertJsonArrayToIntegerList(siblingIds); - // Convert the list of IDs into a list of the actual OrgChartItem objects - List newSiblingItems = newSiblingIdList.stream() - .map(id -> getById(id, orgChartItem)).filter(Objects::nonNull).collect(Collectors.toList()); + // Convert the list of IDs into a list of the actual OrgChartItem objects + List newSiblingItems = + newSiblingIdList.stream().map(id -> getById(id, orgChartItem)).filter(Objects::nonNull) + .collect(Collectors.toList()); - // Fire the event with the parent and the fully populated list of child items - fireSiblingsAddedEvent(targetItem, newSiblingItems, true); + // Fire the event with the parent and the fully populated list of child items + fireSiblingsAddedEvent(targetItem, newSiblingItems, true); + } } /** @@ -504,16 +507,19 @@ public void addChildren(Integer nodeId, List children) { private void onChildrenAdded(String nodeId, JsonArray childIds) { // Find the parent node where children were added OrgChartItem parentItem = getById(Integer.valueOf(nodeId), orgChartItem); + if (parentItem != null) { - // Convert the JsonArray to a simple list of integer IDs - List newChildIdList = convertJsonArrayToIntegerList(childIds); + // Convert the JsonArray to a simple list of integer IDs + List newChildIdList = convertJsonArrayToIntegerList(childIds); - // Convert the list of IDs into a list of the actual OrgChartItem objects - List newChildItems = newChildIdList.stream().map(id -> getById(id, orgChartItem)) - .filter(Objects::nonNull).collect(Collectors.toList()); + // Convert the list of IDs into a list of the actual OrgChartItem objects + List newChildItems = + newChildIdList.stream().map(id -> getById(id, orgChartItem)).filter(Objects::nonNull) + .collect(Collectors.toList()); - // Fire the event with the parent and the fully populated list of child items - fireChildrenAddedEvent(parentItem, newChildItems, true); + // Fire the event with the parent and the fully populated list of child items + fireChildrenAddedEvent(parentItem, newChildItems, true); + } } /** From fa15d7d84c31e40cb8704518202d6b0d18fe5475 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 16:59:29 -0300 Subject: [PATCH 10/11] WIP: update remove node logic when root is removed --- .../com/flowingcode/vaadin/addons/orgchart/OrgChart.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index c996e43..fc5752c 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -570,6 +570,11 @@ public void removeNodes(Integer nodeId) { parentNode.setChildren(currentChildren); } + // If removing the root, clear internal root reference + if (this.orgChartItem != null && nodeId.equals(this.orgChartItem.getId())) { + this.orgChartItem = null; + } + // Update the visual representation this.getElement().executeJs("this.removeNodes($0)", nodeId); } From e13c97b3cdb771bc78064d42107ba75766494c54 Mon Sep 17 00:00:00 2001 From: Paola De Bartolo Date: Tue, 12 Aug 2025 17:26:38 -0300 Subject: [PATCH 11/11] WIP: add missing node not found checks --- .../vaadin/addons/orgchart/OrgChart.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java index fc5752c..918a706 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java +++ b/src/main/java/com/flowingcode/vaadin/addons/orgchart/OrgChart.java @@ -400,7 +400,7 @@ private void appendItemsToParent(OrgChartItem parentNode, List ite * This must not be the root node's ID. * @param siblings a list of {@link OrgChartItem} objects to be added as siblings * @throws IllegalArgumentException if the {@code nodeId} belongs to the root node of the chart, - * as the root cannot have siblings + * as the root cannot have siblings or if the {@code nodeId} is not found in the chart */ public void addSiblings(Integer nodeId, List siblings) { // First check if selected node is not the root node @@ -416,6 +416,8 @@ public void addSiblings(Integer nodeId, List siblings) { // Update parent's children list with the new siblings appendItemsToParent(parentNode, siblings); } + } else { + throw new IllegalArgumentException("Node not found: " + nodeId); } // Update the visual representation by calling the client-side method addSiblings @@ -482,16 +484,18 @@ protected void fireSiblingsAddedEvent(OrgChartItem item, List newS * * @param nodeId the ID of the parent node to which the new children will be added * @param children a list of {@link OrgChartItem} objects to be added as new children + * @throws IllegalArgumentException if the {@code nodeId} is not found in the chart */ public void addChildren(Integer nodeId, List children) { // Update the internal data structure OrgChartItem targetNode = getById(nodeId, orgChartItem); + if (targetNode == null) { + throw new IllegalArgumentException("Node not found: " + nodeId); + } boolean currentChildrenEmpty = targetNode.getChildren() == null || targetNode.getChildren().isEmpty(); - if (targetNode != null) { - // Add new children while preserving existing ones - appendItemsToParent(targetNode, children); - } + // Add new children while preserving existing ones + appendItemsToParent(targetNode, children); // Update the visual representation String itemsJson = convertToJsonObj(children); @@ -555,6 +559,7 @@ protected void fireChildrenAddedEvent(OrgChartItem item, List newC * * @param nodeId the ID of the node to remove. All children and subsequent descendants of this * node will also be removed from the chart. + * @throws IllegalArgumentException if the {@code nodeId} is not found in the chart */ public void removeNodes(Integer nodeId) { // Find the node set for removal @@ -577,6 +582,8 @@ public void removeNodes(Integer nodeId) { // Update the visual representation this.getElement().executeJs("this.removeNodes($0)", nodeId); + } else { + throw new IllegalArgumentException("Node not found: " + nodeId); } } @@ -674,6 +681,7 @@ protected void fireParentAddedEvent(OrgChartItem newParent, boolean fromClient) * @param newDataItem an {@link OrgChartItem} containing the new data to be merged. The ID of this * item is ignored; only its other properties (name, title, custom data, etc) are used for * the update. + * @throws IllegalArgumentException if the {@code nodeId} is not found in the chart */ public void updateNode(Integer nodeId, OrgChartItem newDataItem) { OrgChartItem nodeToUpdate = getById(nodeId, this.orgChartItem); @@ -698,6 +706,8 @@ public void updateNode(Integer nodeId, OrgChartItem newDataItem) { // Call the client-side JS function to update the visual representation String newDataJson = convertToJsonObj(newDataItem); this.getElement().executeJs("this.updateNode($0, $1)", nodeId, newDataJson); + } else { + throw new IllegalArgumentException("Node not found: " + nodeId); } }