From 94e74b7a0d61124aa9c0a5ad02ce146f29fa9a3a Mon Sep 17 00:00:00 2001 From: Jerome Gout Date: Mon, 26 Aug 2024 19:29:18 +0200 Subject: [PATCH] [3882] Add a new tree event to support non explorer tree descriptions + an example of tree representation for Domain elements Bug: https://github.com/eclipse-sirius/sirius-web/issues/3882 Signed-off-by: Jerome Gout --- CHANGELOG.adoc | 3 +- .../services/ExpandAllTreePathProvider.java | 26 +- .../services/ExplorerChildrenProvider.java | 24 -- .../services/ExplorerDescriptionProvider.java | 61 +--- .../tree/DomainExpandAllTreePathProvider.java | 142 +++++++++ .../tree/DomainTreeDescriptionProvider.java | 298 ++++++++++++++++++ .../views/tree/DomainTreePathProvider.java | 60 ++++ .../DomainTreeRepresentationDescription.java | 41 +++ .../api/IGivenCreatedTreeSubscription.java | 29 ++ .../tree/GivenCreatedTreeSubscription.java | 64 ++++ .../tree/TreeEventSubscriptionRunner.java | 50 +++ ...reeNonSemanticElementControllerTests.java} | 116 +++---- .../extension/DefaultExtensionRegistry.tsx | 5 +- .../trees/TreeEventProcessorFactory.java | 111 +++++++ .../collaborative/trees/TreeService.java | 1 + .../trees/api/TreeConfiguration.java | 52 +++ .../trees/api/TreeCreationParameters.java | 14 + .../trees/dto/TreeEventInput.java | 26 ++ .../handlers/CreateTreeEventHandler.java | 117 +++++++ .../trees/services/TreeCreationService.java | 60 ++++ .../trees/services/TreeDeserializer.java | 57 ++++ .../src/main/resources/schema/tree.graphqls | 11 + .../SubscriptionTreeEventDataFetcher.java | 72 +++++ .../sirius-components-trees/src/index.ts | 1 + .../src/trees/TreeRepresentation.tsx | 46 +++ .../src/trees/TreeRepresentation.types.ts | 17 + .../src/trees/useTreeSubscription.tsx | 89 ++++++ .../src/trees/useTreeSubscription.types.ts | 49 +++ 28 files changed, 1489 insertions(+), 153 deletions(-) create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainExpandAllTreePathProvider.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeDescriptionProvider.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreePathProvider.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescription.java create mode 100644 packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/api/IGivenCreatedTreeSubscription.java create mode 100644 packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/GivenCreatedTreeSubscription.java create mode 100644 packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/TreeEventSubscriptionRunner.java rename packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/{ExplorerNonSemanticElementControllerTests.java => TreeNonSemanticElementControllerTests.java} (58%) create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/CreateTreeEventHandler.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeCreationService.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeDeserializer.java create mode 100644 packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/subscription/SubscriptionTreeEventDataFetcher.java create mode 100644 packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.tsx create mode 100644 packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.types.ts create mode 100644 packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.tsx create mode 100644 packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 3ce589df46..78f94c8427 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -40,7 +40,7 @@ - https://github.com/eclipse-sirius/sirius-web/issues/3887[#3887] [diagram] Memoize diagram representation (improve performance when selecting an element on large diagram) - https://github.com/eclipse-sirius/sirius-web/issues/3987[#3987] [sirius-web] Transform `widgetFields` fragment to retrieve custom widget fields - https://github.com/eclipse-sirius/sirius-web/issues/3875[#3875] [sirius-web] Move explorer related code from `sirius-components-trees` to `sirius-web-application` - +- https://github.com/eclipse-sirius/sirius-web/issues/3882[#3882] [sirius-web] Add a new tree event to handle tree description which are not explorer-related == v2024.9.0 @@ -175,7 +175,6 @@ A migration participant has been added to automatically keep compatible all diag - https://github.com/eclipse-sirius/sirius-web/issues/3951[#3951] [sirius-web] Provide an error page to redirect users in case of error - https://github.com/eclipse-sirius/sirius-web/issues/3974[#3974] [diagram] Add support for `<` and `>` to trigger direct edit - == v2024.7.0 === Shapes diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExpandAllTreePathProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExpandAllTreePathProvider.java index 6d5886ea42..a4244f1f7a 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExpandAllTreePathProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExpandAllTreePathProvider.java @@ -13,7 +13,6 @@ package org.eclipse.sirius.web.application.views.explorer.services; import java.util.LinkedHashSet; -import java.util.List; import java.util.Objects; import java.util.Set; @@ -28,7 +27,6 @@ import org.eclipse.sirius.components.core.api.IIdentityService; import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; -import org.eclipse.sirius.components.domain.Entity; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.trees.Tree; import org.eclipse.sirius.components.trees.description.TreeDescription; @@ -74,7 +72,7 @@ public IPayload handle(IEditingContext editingContext, Tree tree, ExpandAllTreeP Set treeItemIdsToExpand = new LinkedHashSet<>(); var object = this.getTreeItemObject(editingContext, tree, treeItemId); - if (object instanceof EObject || treeItemId.startsWith(ExplorerDescriptionProvider.SETTING)) { + if (object instanceof EObject) { // We need to get the current depth of the tree item var itemAncestors = this.explorerNavigationService.getAncestors(editingContext, tree, treeItemId); maxDepth = itemAncestors.size(); @@ -99,27 +97,7 @@ private int addAllContents(IEditingContext editingContext, String treeItemId, in var depthConsidered = depth; var object = this.getTreeItemObject(editingContext, tree, treeItemId); - if (treeItemId.startsWith(ExplorerDescriptionProvider.SETTING) && object instanceof List list) { - treeItemIdsToExpand.add(treeItemId); - for (var child : list) { - String childId = this.identityService.getId(child); - treeItemIdsToExpand.add(childId); - var childTreePathMaxDepth = depth + 1; - childTreePathMaxDepth = this.addAllContents(editingContext, childId, childTreePathMaxDepth, treeItemIdsToExpand, tree); - depthConsidered = Math.max(depthConsidered, childTreePathMaxDepth); - } - } else if (object instanceof EObject eObject) { - if (object instanceof Entity entity) { - // an Entity has a virtual node for its super types, this node should be a child of the Entity - var id = ExplorerDescriptionProvider.SETTING + this.identityService.getId(entity) + ExplorerDescriptionProvider.SETTING_ID_SEPARATOR + "superTypes"; - treeItemIdsToExpand.add(id); - var superTypes = entity.getSuperTypes(); - if (superTypes.size() > 0) { - depthConsidered = Math.max(depthConsidered, depth + 2); - } else { - depthConsidered = Math.max(depthConsidered, depth + 1); - } - } + if (object instanceof EObject eObject) { var contents = this.contentService.getContents(eObject); if (!contents.isEmpty()) { treeItemIdsToExpand.add(treeItemId); diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerChildrenProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerChildrenProvider.java index 82dcccd813..7fe5d6a937 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerChildrenProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerChildrenProvider.java @@ -13,20 +13,16 @@ package org.eclipse.sirius.web.application.views.explorer.services; import java.util.ArrayList; -import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Objects; import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.EStructuralFeature.Setting; -import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.sirius.components.core.RepresentationMetadata; import org.eclipse.sirius.components.core.api.IEditingContext; import org.eclipse.sirius.components.core.api.IObjectService; import org.eclipse.sirius.components.core.api.IRepresentationMetadataSearchService; -import org.eclipse.sirius.components.domain.Entity; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.trees.renderer.TreeRenderer; import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerChildrenProvider; @@ -71,15 +67,6 @@ public boolean hasChildren(VariableManager variableManager) { String id = this.objectService.getId(eObject); hasChildren = this.representationDataSearchService.existAnyRepresentationForTargetObjectId(id); } - - if (!hasChildren && self instanceof Entity) { - hasChildren = true; - } - } else if (self instanceof Setting setting) { - var value = setting.get(true); - if (value instanceof Collection collection) { - hasChildren = !collection.isEmpty(); - } } return hasChildren; } @@ -130,17 +117,8 @@ private List getDefaultChildren(VariableManager variableManager) { representationMetadata.sort(Comparator.comparing(RepresentationMetadata::getLabel)); result.addAll(representationMetadata); List contents = this.objectService.getContents(self); - if (self instanceof Entity entity) { - result.add(((InternalEObject) entity).eSetting(entity.eClass().getEStructuralFeature("superTypes"))); - } result.addAll(contents); - } else if (self instanceof Setting setting) { - var value = setting.get(true); - if (value instanceof Collection collection) { - result.addAll(collection); - } } - } } return result; @@ -167,8 +145,6 @@ private String getTreeItemId(VariableManager variableManager) { id = resource.getURI().path().substring(1); } else if (self instanceof EObject) { id = this.objectService.getId(self); - } else if (self instanceof Setting setting) { - id = ExplorerDescriptionProvider.SETTING + this.objectService.getId(setting.getEObject()) + ExplorerDescriptionProvider.SETTING_ID_SEPARATOR + setting.getEStructuralFeature().getName(); } return id; } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java index 68c8bec8eb..d46555effd 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java @@ -20,8 +20,6 @@ import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; -import org.eclipse.emf.ecore.EStructuralFeature.Setting; -import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.sirius.components.collaborative.api.IRepresentationImageProvider; @@ -70,10 +68,6 @@ public class ExplorerDescriptionProvider implements IEditingContextRepresentatio public static final String REPRESENTATION_NAME = "Explorer"; - public static final String SETTING = "setting:"; - - public static final String SETTING_ID_SEPARATOR = "::"; - private final IObjectService objectService; private final IURLParser urlParser; @@ -160,8 +154,6 @@ private String getTreeItemId(VariableManager variableManager) { id = resource.getURI().path().substring(1); } else if (self instanceof EObject) { id = this.objectService.getId(self); - } else if (self instanceof Setting setting) { - id = SETTING + this.objectService.getId(setting.getEObject()) + SETTING_ID_SEPARATOR + setting.getEStructuralFeature().getName(); } return id; } @@ -173,8 +165,6 @@ private String getKind(VariableManager variableManager) { kind = representationMetadata.getKind(); } else if (self instanceof Resource) { kind = DOCUMENT_KIND; - } else if (self instanceof Setting) { - kind = "setting"; } else { kind = this.objectService.getKind(self); } @@ -197,8 +187,6 @@ private StyledString getLabel(VariableManager variableManager) { var kind = this.objectService.getKind(self); label = this.urlParser.getParameterValues(kind).get(SemanticKindConstants.ENTITY_ARGUMENT).get(0); } - } else if (self instanceof Setting setting) { - label = setting.getEStructuralFeature().getName(); } return StyledString.of(label); } @@ -302,36 +290,24 @@ private Object getTreeItemObject(VariableManager variableManager) { if (optionalEditingContext.isPresent() && optionalTreeItemId.isPresent()) { var treeItemId = optionalTreeItemId.get(); var editingContext = optionalEditingContext.get(); - - if (treeItemId.startsWith(SETTING)) { - // the tree item is a setting, get the object and then the structural feature associated - var objectId = treeItemId.substring(SETTING.length(), treeItemId.indexOf(SETTING_ID_SEPARATOR)); - var featureName = treeItemId.substring(treeItemId.indexOf(SETTING_ID_SEPARATOR) + SETTING_ID_SEPARATOR.length()); - var optObject = this.objectService.getObject(editingContext, objectId); - if (optObject.isPresent()) { - InternalEObject internalObject = (InternalEObject) optObject.get(); - result = internalObject.eSetting(internalObject.eClass().getEStructuralFeature(featureName)); - } + var optionalObject = this.objectService.getObject(editingContext, treeItemId); + if (optionalObject.isPresent()) { + result = optionalObject.get(); } else { - var optionalObject = this.objectService.getObject(editingContext, treeItemId); - if (optionalObject.isPresent()) { - result = optionalObject.get(); - } else { - var optionalEditingDomain = Optional.of(editingContext) - .filter(IEMFEditingContext.class::isInstance) - .map(IEMFEditingContext.class::cast) - .map(IEMFEditingContext::getDomain); - - if (optionalEditingDomain.isPresent()) { - var editingDomain = optionalEditingDomain.get(); - ResourceSet resourceSet = editingDomain.getResourceSet(); - URI uri = new JSONResourceFactory().createResourceURI(treeItemId); - - result = resourceSet.getResources().stream() - .filter(resource -> resource.getURI().equals(uri)) - .findFirst() - .orElse(null); - } + var optionalEditingDomain = Optional.of(editingContext) + .filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast) + .map(IEMFEditingContext::getDomain); + + if (optionalEditingDomain.isPresent()) { + var editingDomain = optionalEditingDomain.get(); + ResourceSet resourceSet = editingDomain.getResourceSet(); + URI uri = new JSONResourceFactory().createResourceURI(treeItemId); + + result = resourceSet.getResources().stream() + .filter(resource -> resource.getURI().equals(uri)) + .findFirst() + .orElse(null); } } } @@ -354,9 +330,6 @@ private Object getParentObject(VariableManager variableManager) { semanticContainer = eObject.eResource(); } result = semanticContainer; - } else if (self instanceof Setting setting) { - // the parent of the superTypes node is the object associated to this Setting - result = setting.getEObject(); } return result; } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainExpandAllTreePathProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainExpandAllTreePathProvider.java new file mode 100644 index 0000000000..002645790c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainExpandAllTreePathProvider.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.tree; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.collaborative.trees.api.IExpandAllTreePathProvider; +import org.eclipse.sirius.components.collaborative.trees.dto.ExpandAllTreePathInput; +import org.eclipse.sirius.components.collaborative.trees.dto.ExpandAllTreePathSuccessPayload; +import org.eclipse.sirius.components.collaborative.trees.dto.TreePath; +import org.eclipse.sirius.components.core.api.IContentService; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; +import org.eclipse.sirius.components.domain.Entity; +import org.eclipse.sirius.components.representations.VariableManager; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerNavigationService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataSearchService; +import org.springframework.stereotype.Service; + +/** + * ExpandAll tree path provider for Domain tree representation example. + * + * @author Jerome Gout + */ +@Service +public class DomainExpandAllTreePathProvider implements IExpandAllTreePathProvider { + + private final IIdentityService identityService; + + private final IContentService contentService; + + private final IExplorerNavigationService explorerNavigationService; + + private final IRepresentationDataSearchService representationDataSearchService; + + private final IRepresentationDescriptionSearchService representationDescriptionSearchService; + public DomainExpandAllTreePathProvider(IIdentityService identityService, IContentService contentService, IExplorerNavigationService explorerNavigationService, IRepresentationDescriptionSearchService representationDescriptionSearchService, IRepresentationDataSearchService representationDataSearchService) { + this.identityService = Objects.requireNonNull(identityService); + this.contentService = Objects.requireNonNull(contentService); + this.explorerNavigationService = Objects.requireNonNull(explorerNavigationService); + this.representationDescriptionSearchService = Objects.requireNonNull(representationDescriptionSearchService); + this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + } + + @Override + public boolean canHandle(Tree tree) { + return tree.getDescriptionId().equals(DomainTreeDescriptionProvider.DESCRIPTION_ID); + } + + @Override + public IPayload handle(IEditingContext editingContext, Tree tree, ExpandAllTreePathInput input) { + int maxDepth = 0; + String treeItemId = input.treeItemId(); + + Set treeItemIdsToExpand = new LinkedHashSet<>(); + var object = this.getTreeItemObject(editingContext, tree, treeItemId); + if (object instanceof EObject || treeItemId.startsWith(DomainTreeDescriptionProvider.SETTING)) { + // We need to get the current depth of the tree item + var itemAncestors = this.explorerNavigationService.getAncestors(editingContext, tree, treeItemId); + maxDepth = itemAncestors.size(); + maxDepth = this.addAllContents(editingContext, treeItemId, maxDepth, treeItemIdsToExpand, tree); + } + return new ExpandAllTreePathSuccessPayload(input.id(), new TreePath(treeItemIdsToExpand.stream().toList(), maxDepth)); + } + + private int addAllContents(IEditingContext editingContext, String treeItemId, int depth, Set treeItemIdsToExpand, Tree tree) { + var depthConsidered = depth; + var object = this.getTreeItemObject(editingContext, tree, treeItemId); + if (treeItemId.startsWith(DomainTreeDescriptionProvider.SETTING) && object instanceof List list) { + treeItemIdsToExpand.add(treeItemId); + for (var child : list) { + String childId = this.identityService.getId(child); + treeItemIdsToExpand.add(childId); + var childTreePathMaxDepth = depth + 1; + childTreePathMaxDepth = this.addAllContents(editingContext, childId, childTreePathMaxDepth, treeItemIdsToExpand, tree); + depthConsidered = Math.max(depthConsidered, childTreePathMaxDepth); + } + } else if (object instanceof EObject eObject) { + if (object instanceof Entity entity) { + // an Entity has a virtual node for its super types, this node should be a child of the Entity + var id = DomainTreeDescriptionProvider.SETTING + this.identityService.getId(entity) + DomainTreeDescriptionProvider.SETTING_ID_SEPARATOR + "superTypes"; + treeItemIdsToExpand.add(id); + var superTypes = entity.getSuperTypes(); + if (superTypes.size() > 0) { + depthConsidered = Math.max(depthConsidered, depth + 2); + } else { + depthConsidered = Math.max(depthConsidered, depth + 1); + } + } + var contents = this.contentService.getContents(eObject); + if (!contents.isEmpty()) { + treeItemIdsToExpand.add(treeItemId); + + for (var child : contents) { + String childId = this.identityService.getId(child); + treeItemIdsToExpand.add(childId); + var childTreePathMaxDepth = depth + 1; + childTreePathMaxDepth = this.addAllContents(editingContext, childId, childTreePathMaxDepth, treeItemIdsToExpand, tree); + depthConsidered = Math.max(depthConsidered, childTreePathMaxDepth); + } + } else if (this.representationDataSearchService.existAnyRepresentationForTargetObjectId(treeItemId)) { + treeItemIdsToExpand.add(treeItemId); + depthConsidered = Math.max(depthConsidered, depth + 1); + } + } + + return depthConsidered; + } + + private Object getTreeItemObject(IEditingContext editingContext, Tree tree, String treeItemId) { + var optionalTreeDescription = this.representationDescriptionSearchService.findById(editingContext, tree.getDescriptionId()) + .filter(TreeDescription.class::isInstance) + .map(TreeDescription.class::cast); + + if (optionalTreeDescription.isPresent()) { + var variableManager = new VariableManager(); + variableManager.put(IEditingContext.EDITING_CONTEXT, editingContext); + variableManager.put(TreeDescription.ID, treeItemId); + return optionalTreeDescription.get().getTreeItemObjectProvider().apply(variableManager); + } + return null; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeDescriptionProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeDescriptionProvider.java new file mode 100644 index 0000000000..0b77f47dc5 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeDescriptionProvider.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.tree; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature.Setting; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.sirius.components.collaborative.api.IRepresentationImageProvider; +import org.eclipse.sirius.components.core.CoreImageConstants; +import org.eclipse.sirius.components.core.RepresentationMetadata; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IEditingContextRepresentationDescriptionProvider; +import org.eclipse.sirius.components.core.api.IObjectService; +import org.eclipse.sirius.components.core.api.IRepresentationMetadataSearchService; +import org.eclipse.sirius.components.core.api.IURLParser; +import org.eclipse.sirius.components.core.api.SemanticKindConstants; +import org.eclipse.sirius.components.core.api.labels.StyledString; +import org.eclipse.sirius.components.domain.Domain; +import org.eclipse.sirius.components.domain.Entity; +import org.eclipse.sirius.components.representations.GetOrCreateRandomIdProvider; +import org.eclipse.sirius.components.representations.IRepresentationDescription; +import org.eclipse.sirius.components.representations.Success; +import org.eclipse.sirius.components.representations.VariableManager; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.eclipse.sirius.components.trees.renderer.TreeRenderer; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.projections.RepresentationDataMetadataOnly; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataSearchService; +import org.springframework.stereotype.Service; + +/** + * This class is used to provide the description of a tree on Domain element.
+ * This is an example to demonstrate how to use a tree description which is not a tree explorer. + * + * @author Jerome Gout + */ +@Service +public class DomainTreeDescriptionProvider implements IEditingContextRepresentationDescriptionProvider { + + public static final String DESCRIPTION_ID = "domain_tree_description"; + + public static final String SETTING = "setting:"; + + public static final String SETTING_ID_SEPARATOR = "::"; + + private final IObjectService objectService; + + private final IURLParser urlParser; + + private final List representationImageProviders; + + private final IRepresentationMetadataSearchService representationMetadataSearchService; + + private final IRepresentationDataSearchService representationDataSearchService; + + public DomainTreeDescriptionProvider(IObjectService objectService, IURLParser urlParser, List representationImageProviders, IRepresentationDataSearchService representationDataSearchService, IRepresentationMetadataSearchService representationMetadataSearchService) { + this.objectService = Objects.requireNonNull(objectService); + this.urlParser = Objects.requireNonNull(urlParser); + this.representationImageProviders = Objects.requireNonNull(representationImageProviders); + this.representationMetadataSearchService = Objects.requireNonNull(representationMetadataSearchService); + this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + } + + @Override + public List getRepresentationDescriptions(IEditingContext editingContext) { + + var treeDescription = TreeDescription.newTreeDescription(DESCRIPTION_ID) + .label("Domain Tree") + .idProvider(new GetOrCreateRandomIdProvider()) + .canCreatePredicate(this::canCreate) + .treeItemIdProvider(this::getTreeItemId) + .kindProvider(this::getKind) + .labelProvider(this::getLabel) + .targetObjectIdProvider(variableManager -> variableManager.get(VariableManager.SELF, Object.class).map(this.objectService::getId).orElse(null)) + .iconURLProvider(this::getImageURL) + .editableProvider(variableManager -> false) + .deletableProvider(variableManager -> false) + .selectableProvider(variableManager -> true) + .elementsProvider(this::getElements) + .hasChildrenProvider(this::hasChildren) + .childrenProvider(this::getChildren) + .parentObjectProvider(this::getParentObject) + .deleteHandler(variableManager -> new Success()) + .renameHandler((variableManager, newName) -> new Success()) + .treeItemObjectProvider(this::getTreeItemObject) + .build(); + return List.of(treeDescription); + } + + private boolean canCreate(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + return self instanceof Domain; + } + + private String getTreeItemId(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + + String id = null; + if (self instanceof RepresentationMetadata representationMetadata) { + id = representationMetadata.getId(); + } else if (self instanceof EObject) { + id = this.objectService.getId(self); + } else if (self instanceof Setting setting) { + id = SETTING + this.objectService.getId(setting.getEObject()) + SETTING_ID_SEPARATOR + setting.getEStructuralFeature().getName(); + } + return id; + } + + private String getKind(VariableManager variableManager) { + String kind = ""; + Object self = variableManager.getVariables().get(VariableManager.SELF); + if (self instanceof RepresentationMetadata representationMetadata) { + kind = representationMetadata.getKind(); + } else if (self instanceof Setting) { + kind = "setting"; + } else { + kind = this.objectService.getKind(self); + } + return kind; + } + + private StyledString getLabel(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + + String label = ""; + if (self instanceof RepresentationMetadata representationMetadata) { + label = representationMetadata.getLabel(); + } else if (self instanceof EObject) { + label = this.objectService.getLabel(self); + if (label.isBlank()) { + var kind = this.objectService.getKind(self); + label = this.urlParser.getParameterValues(kind).get(SemanticKindConstants.ENTITY_ARGUMENT).get(0); + } + } else if (self instanceof Setting setting) { + label = setting.getEStructuralFeature().getName(); + } + return StyledString.of(label); + } + + private List getImageURL(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + + List imageURL = List.of(CoreImageConstants.DEFAULT_SVG); + if (self instanceof EObject) { + imageURL = this.objectService.getImagePath(self); + } else if (self instanceof RepresentationMetadata representationMetadata) { + imageURL = this.representationImageProviders.stream() + .map(representationImageProvider -> representationImageProvider.getImageURL(representationMetadata.getKind())) + .flatMap(Optional::stream) + .toList(); + } + return imageURL; + } + + private List getElements(VariableManager variableManager) { + List elements = new ArrayList<>(); + Object self = variableManager.getVariables().get(VariableManager.SELF); + if (self instanceof Domain domain) { + elements.add(domain); + } + return elements; + } + + private boolean hasChildren(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + + boolean hasChildren = false; + if (self instanceof EObject eObject) { + hasChildren = !eObject.eContents().isEmpty(); + + if (!hasChildren) { + String id = this.objectService.getId(eObject); + hasChildren = this.representationDataSearchService.existAnyRepresentationForTargetObjectId(id); + } + + if (!hasChildren && self instanceof Entity) { + hasChildren = true; + } + } else if (self instanceof Setting setting) { + var value = setting.get(true); + if (value instanceof Collection collection) { + hasChildren = !collection.isEmpty(); + } + } + return hasChildren; + } + + public List getChildren(VariableManager variableManager) { + List children = new ArrayList<>(); + Object self = variableManager.getVariables().get(VariableManager.SELF); + if (self != null) { + children = this.getDefaultChildren(variableManager); + } + return children; + } + + private List getDefaultChildren(VariableManager variableManager) { + List result = new ArrayList<>(); + var optionalEditingContext = variableManager.get(IEditingContext.EDITING_CONTEXT, IEditingContext.class); + Object self = variableManager.getVariables().get(VariableManager.SELF); + List expandedIds = new ArrayList<>(); + Object objects = variableManager.getVariables().get(TreeRenderer.EXPANDED); + if (objects instanceof List list) { + expandedIds = list.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + } + if (optionalEditingContext.isPresent()) { + IEditingContext editingContext = optionalEditingContext.get(); + String id = this.getTreeItemId(variableManager); + if (expandedIds.contains(id)) { + if (self instanceof EObject) { + var representationMetadata = new ArrayList<>(this.representationMetadataSearchService.findAllByTargetObjectId(editingContext, id)); + representationMetadata.sort(Comparator.comparing(RepresentationMetadata::getLabel)); + result.addAll(representationMetadata); + List contents = this.objectService.getContents(self); + if (self instanceof Entity entity) { + result.add(((InternalEObject) entity).eSetting(entity.eClass().getEStructuralFeature("superTypes"))); + } + result.addAll(contents); + } else if (self instanceof Setting setting) { + var value = setting.get(true); + if (value instanceof Collection collection) { + result.addAll(collection); + } + } + } + } + return result; + } + + private Object getTreeItemObject(VariableManager variableManager) { + Object result = null; + var optionalTreeItemId = variableManager.get(TreeDescription.ID, String.class); + var optionalEditingContext = variableManager.get(IEditingContext.EDITING_CONTEXT, IEditingContext.class); + + if (optionalEditingContext.isPresent() && optionalTreeItemId.isPresent()) { + var treeItemId = optionalTreeItemId.get(); + var editingContext = optionalEditingContext.get(); + + if (treeItemId.startsWith(SETTING)) { + // the tree item is a setting, get the object and then the structural feature associated + var objectId = treeItemId.substring(SETTING.length(), treeItemId.indexOf(SETTING_ID_SEPARATOR)); + var featureName = treeItemId.substring(treeItemId.indexOf(SETTING_ID_SEPARATOR) + SETTING_ID_SEPARATOR.length()); + var optObject = this.objectService.getObject(editingContext, objectId); + if (optObject.isPresent()) { + InternalEObject internalObject = (InternalEObject) optObject.get(); + result = internalObject.eSetting(internalObject.eClass().getEStructuralFeature(featureName)); + } + } else { + var optionalObject = this.objectService.getObject(editingContext, treeItemId); + if (optionalObject.isPresent()) { + result = optionalObject.get(); + } + } + } + + return result; + } + + private Object getParentObject(VariableManager variableManager) { + Object self = variableManager.getVariables().get(VariableManager.SELF); + var optionalTreeItemId = variableManager.get(TreeDescription.ID, String.class); + var optionalEditingContext = variableManager.get(IEditingContext.EDITING_CONTEXT, IEditingContext.class); + Object result = null; + + if (self instanceof RepresentationMetadata && optionalTreeItemId.isPresent() && optionalEditingContext.isPresent()) { + var optionalRepresentationMetadata = new UUIDParser().parse(optionalTreeItemId.get()).flatMap(this.representationDataSearchService::findMetadataById); + var repId = optionalRepresentationMetadata.map(RepresentationDataMetadataOnly::targetObjectId).orElse(null); + result = this.objectService.getObject(optionalEditingContext.get(), repId); + } else if (self instanceof EObject eObject) { + Object semanticContainer = eObject.eContainer(); + if (semanticContainer == null) { + semanticContainer = eObject.eResource(); + } + result = semanticContainer; + } + return result; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreePathProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreePathProvider.java new file mode 100644 index 0000000000..54795a1c7d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreePathProvider.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.tree; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.sirius.components.collaborative.trees.api.ITreePathProvider; +import org.eclipse.sirius.components.collaborative.trees.dto.TreePath; +import org.eclipse.sirius.components.collaborative.trees.dto.TreePathInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreePathSuccessPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerNavigationService; +import org.springframework.stereotype.Service; + +/** + * Implementation of {@link ITreePathProvider} for the example Domain tree. + * + * @author Jerome Gout + */ +@Service +public class DomainTreePathProvider implements ITreePathProvider { + + private final IExplorerNavigationService explorerNavigationService; + + public DomainTreePathProvider(IExplorerNavigationService explorerNavigationService) { + this.explorerNavigationService = Objects.requireNonNull(explorerNavigationService); + } + + @Override + public boolean canHandle(Tree tree) { + return tree.getDescriptionId().equals(DomainTreeDescriptionProvider.DESCRIPTION_ID); + } + + @Override + public IPayload handle(IEditingContext editingContext, Tree tree, TreePathInput input) { + int maxDepth = 0; + Set allAncestors = new LinkedHashSet<>(); + for (String selectionEntryId : input.selectionEntryIds()) { + List itemAncestors = this.explorerNavigationService.getAncestors(editingContext, tree, selectionEntryId); + allAncestors.addAll(itemAncestors); + maxDepth = Math.max(maxDepth, itemAncestors.size()); + } + return new TreePathSuccessPayload(input.id(), new TreePath(allAncestors.stream().toList(), maxDepth)); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescription.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescription.java new file mode 100644 index 0000000000..18bc0f1365 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescription.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.tree; + +import java.util.List; + +import org.eclipse.sirius.components.collaborative.api.IRepresentationDescriptionsProvider; +import org.eclipse.sirius.components.collaborative.api.RepresentationDescriptionMetadata; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.representations.IRepresentationDescription; +import org.springframework.stereotype.Service; + +/** + * Used to make the example Domain tree description work. + * + * @author Jerome Gout + */ +@Service +public class DomainTreeRepresentationDescription implements IRepresentationDescriptionsProvider { + + @Override + public boolean canHandle(IRepresentationDescription representationDescription) { + return representationDescription.getId().equals(DomainTreeDescriptionProvider.DESCRIPTION_ID); + } + + @Override + public List handle(IEditingContext editingContext, Object object, IRepresentationDescription representationDescription) { + return List.of(new RepresentationDescriptionMetadata(representationDescription.getId(), representationDescription.getLabel(), representationDescription.getLabel())); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/api/IGivenCreatedTreeSubscription.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/api/IGivenCreatedTreeSubscription.java new file mode 100644 index 0000000000..6cdb0930c1 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/api/IGivenCreatedTreeSubscription.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.tests.services.api; + +import java.util.List; + +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; + +import reactor.core.publisher.Flux; + +/** + * Used to create a tree and subscribe to it. + * + * @author Jerome Gout + */ +public interface IGivenCreatedTreeSubscription { + + Flux createAndSubscribe(CreateRepresentationInput input, List expandedIds); +} diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/GivenCreatedTreeSubscription.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/GivenCreatedTreeSubscription.java new file mode 100644 index 0000000000..a74290a6d2 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/GivenCreatedTreeSubscription.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.tests.services.tree; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; +import org.eclipse.sirius.web.tests.services.api.IGivenCommittedTransaction; +import org.eclipse.sirius.web.tests.services.api.IGivenCreatedRepresentation; +import org.eclipse.sirius.web.tests.services.api.IGivenCreatedTreeSubscription; +import org.springframework.stereotype.Service; +import org.springframework.test.context.transaction.TestTransaction; + +import reactor.core.publisher.Flux; + +/** + * Used to create a tree and subscribe to it. + * + * @author Jerome Gout + */ +@Service +public class GivenCreatedTreeSubscription implements IGivenCreatedTreeSubscription { + + private final IGivenCommittedTransaction givenCommittedTransaction; + + private final IGivenCreatedRepresentation givenCreatedRepresentation; + + private final TreeEventSubscriptionRunner treeEventSubscriptionRunner; + + public GivenCreatedTreeSubscription(IGivenCommittedTransaction givenCommittedTransaction, IGivenCreatedRepresentation givenCreatedRepresentation, TreeEventSubscriptionRunner treeEventSubscriptionRunner) { + this.givenCommittedTransaction = Objects.requireNonNull(givenCommittedTransaction); + this.givenCreatedRepresentation = Objects.requireNonNull(givenCreatedRepresentation); + this.treeEventSubscriptionRunner = Objects.requireNonNull(treeEventSubscriptionRunner); + } + + @Override + public Flux createAndSubscribe(CreateRepresentationInput input, List expandedIds) { + this.givenCommittedTransaction.commit(); + + String representationId = this.givenCreatedRepresentation.createRepresentation(input); + + var treeEventInput = new TreeEventInput(UUID.randomUUID(), input.editingContextId(), representationId, expandedIds); + var flux = this.treeEventSubscriptionRunner.run(treeEventInput); + + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + + return flux; + } +} diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/TreeEventSubscriptionRunner.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/TreeEventSubscriptionRunner.java new file mode 100644 index 0000000000..e559e8c11b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/services/tree/TreeEventSubscriptionRunner.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.tests.services.tree; + +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.ISubscriptionRunner; +import org.springframework.stereotype.Service; + +import reactor.core.publisher.Flux; + +/** + * Used to get the tree event subscription with the GraphQL API. + * + * @author Jerome Gout + */ +@Service +public class TreeEventSubscriptionRunner implements ISubscriptionRunner { + + private static final String TREE_EVENT_SUBSCRIPTION = """ + subscription treeEvent($input: TreeEventInput!) { + treeEvent(input: $input) { + __typename + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public TreeEventSubscriptionRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public Flux run(TreeEventInput input) { + return this.graphQLRequestor.subscribe(TREE_EVENT_SUBSCRIPTION, input); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerNonSemanticElementControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java similarity index 58% rename from packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerNonSemanticElementControllerTests.java rename to packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java index 8f9c7e1c36..f4ceb60a70 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerNonSemanticElementControllerTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java @@ -25,16 +25,17 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; import org.eclipse.sirius.components.collaborative.trees.dto.TreeRefreshedEventPayload; import org.eclipse.sirius.components.trees.Tree; -import org.eclipse.sirius.components.trees.TreeItem; import org.eclipse.sirius.components.trees.tests.graphql.ExpandAllTreePathQueryRunner; import org.eclipse.sirius.web.AbstractIntegrationTests; -import org.eclipse.sirius.web.application.views.explorer.ExplorerEventInput; -import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; +import org.eclipse.sirius.web.application.views.tree.DomainTreeDescriptionProvider; import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.tests.services.api.IGivenCreatedTreeSubscription; import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; -import org.eclipse.sirius.web.tests.services.explorer.ExplorerEventSubscriptionRunner; +import org.eclipse.sirius.web.tests.services.tree.TreeEventSubscriptionRunner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -45,23 +46,31 @@ import org.springframework.transaction.annotation.Transactional; import graphql.execution.DataFetcherResult; +import reactor.core.publisher.Flux; import reactor.test.StepVerifier; /** - * Integration tests of a non semantic element in the explorer. + * Integration tests of a non semantic element in a tree representation. * * @author Jerome Gout */ @Transactional @SuppressWarnings("checkstyle:MultipleStringLiterals") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ExplorerNonSemanticElementControllerTests extends AbstractIntegrationTests { +public class TreeNonSemanticElementControllerTests extends AbstractIntegrationTests { + + private static final String ROOT_ENTITY_ID = "c341bf91-d315-4264-9787-c51b121a6375"; + + private static final String ROOT_SETTING_ID = "setting:c341bf91-d315-4264-9787-c51b121a6375::superTypes"; @Autowired private IGivenInitialServerState givenInitialServerState; @Autowired - private ExplorerEventSubscriptionRunner treeEventSubscriptionRunner; + private IGivenCreatedTreeSubscription givenCreatedTreeSubscription; + + @Autowired + private TreeEventSubscriptionRunner treeEventSubscriptionRunner; @Autowired private ExpandAllTreePathQueryRunner expandAllTreePathQueryRunner; @@ -71,91 +80,84 @@ public void beforeEach() { this.givenInitialServerState.initialize(); } + private Flux givenSubscriptionToTree() { + var input = new CreateRepresentationInput( + UUID.randomUUID(), + StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), + DomainTreeDescriptionProvider.DESCRIPTION_ID, + StudioIdentifiers.DOMAIN_OBJECT.toString(), + "Tree" + ); + return this.givenCreatedTreeSubscription.createAndSubscribe(input, List.of()); + } + @Test - @DisplayName("Given a sample studio, when we expand all the domain elements, then underneath entities have a superTypes child") + @DisplayName("Given a domain tree representation, when we subscribe to its event, then the representation data contains a superTypes node") @Sql(scripts = {"/scripts/studio.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) - public void givenStudioWhenWeAskForTheTreePathOfPapayaViewObjectThenItsPathInTheExplorerIsReturned() { - var input = new ExplorerEventInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), ExplorerDescriptionProvider.PREFIX, List.of(), List.of()); - var flux = this.treeEventSubscriptionRunner.run(input); + public void givenADomainTreeRepresentationWhenWeSubscribeToItsEventThenTheRepresentationDataContainsASuperTypesNode() { + +// StudioIdentifiers.DOMAIN_OBJECT.toString(), ROOT_ENTITY_ID + var flux = this.givenSubscriptionToTree(); var treeId = new AtomicReference(); - var domainDocumentId = new AtomicReference(); + var settingId = new AtomicReference(); - Consumer initialTreeContentConsumer = this.getTreeSubscriptionConsumer(tree -> { + var initialTreeContentConsumer = this.getTreeSubscriptionConsumer(tree -> { assertThat(tree).isNotNull(); - assertThat(tree.getChildren()).hasSize(5); - assertThat(tree.getChildren()).allSatisfy(treeItem -> assertThat(treeItem.getChildren()).isEmpty()); - + assertThat(tree.getChildren()).hasSize(1); treeId.set(tree.getId()); - domainDocumentId.set(tree.getChildren().get(1).getId()); // Domain document is the second child of the tree }); - // Expand the part of the tree corresponding to the Domain document var treeItemIds = new AtomicReference>(); - Runnable getTreePath = () -> { + Runnable getTreePathFromSetting = () -> { Map variables = Map.of( "editingContextId", StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), "treeId", treeId.get(), - "treeItemId", domainDocumentId.get() + "treeItemId", ROOT_SETTING_ID ); var result = this.expandAllTreePathQueryRunner.run(variables); List treeItemIdsToExpand = JsonPath.read(result, "$.data.viewer.editingContext.expandAllTreePath.treeItemIdsToExpand"); assertThat(treeItemIdsToExpand).isNotEmpty(); - // there are 3 entities in the Domain document, so should get 3 superType treeItems - assertThat(treeItemIdsToExpand.stream() - .filter(id -> id.startsWith(ExplorerDescriptionProvider.SETTING)) - .toList() - ).hasSize(3); + assertThat(treeItemIdsToExpand.stream().filter(id -> id.startsWith(DomainTreeDescriptionProvider.SETTING)).toList()).hasSize(1); + }; + + Runnable getTreePath = () -> { + Map variables = Map.of( + "editingContextId", StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), + "treeId", treeId.get(), + "treeItemId", StudioIdentifiers.DOMAIN_OBJECT.toString() + ); + var result = this.expandAllTreePathQueryRunner.run(variables); + List treeItemIdsToExpand = JsonPath.read(result, "$.data.viewer.editingContext.expandAllTreePath.treeItemIdsToExpand"); + assertThat(treeItemIdsToExpand).isNotEmpty(); + assertThat(treeItemIdsToExpand.stream().filter(id -> id.startsWith(DomainTreeDescriptionProvider.SETTING)).toList()).hasSize(3); + treeItemIds.set(treeItemIdsToExpand); }; StepVerifier.create(flux) .consumeNextWith(initialTreeContentConsumer) + .then(getTreePathFromSetting) .then(getTreePath) .thenCancel() .verify(Duration.ofSeconds(10)); - - var expandedTreeInput = new ExplorerEventInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), ExplorerDescriptionProvider.PREFIX, treeItemIds.get(), List.of()); - var expandedTreeFlux = this.treeEventSubscriptionRunner.run(expandedTreeInput); - var rootTreeItemId = new AtomicReference(); + var expandedTreeInput = new TreeEventInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), treeId.get(), List.of(StudioIdentifiers.DOMAIN_OBJECT.toString(), ROOT_ENTITY_ID)); + Flux expandedTreeFlux = this.treeEventSubscriptionRunner.run(expandedTreeInput); Consumer initialExpandedTreeContentConsumer = this.getTreeSubscriptionConsumer(tree -> { assertThat(tree).isNotNull(); - assertThat(tree.getChildren()).isNotEmpty(); - assertThat(tree.getChildren()).hasSize(5); - var domainDocument = tree.getChildren().get(1); - assertThat(domainDocument).isNotNull(); - assertThat(domainDocument.getChildren()).isNotEmpty(); - assertThat(domainDocument.getChildren()).hasSize(1); - assertThat(domainDocument.getChildren().get(0)).isNotNull(); - List domainElements = domainDocument.getChildren().get(0).getChildren(); - assertThat(domainElements).hasSize(3); - assertThat(domainElements.get(0).getLabel().toString()).isEqualTo("Root"); - rootTreeItemId.set(domainElements.get(0).getId()); - assertThat(domainElements.get(0).getChildren()).isNotEmpty(); - assertThat(domainElements.get(0).getChildren()).hasSize(3); - assertThat(domainElements.get(0).getChildren().get(0).getLabel().toString()).isEqualTo("superTypes"); + assertThat(tree.getChildren()).hasSize(1); + assertThat(tree.getChildren().get(0).getChildren()).hasSize(4); + assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).hasSize(3); + assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren().get(0).getId()).startsWith(DomainTreeDescriptionProvider.SETTING); + settingId.set(tree.getChildren().get(0).getChildren().get(1).getChildren().get(0).getId()); }); - Runnable getTreePathFromRoot = () -> { - Map variables = Map.of( - "editingContextId", StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), - "treeId", treeId.get(), - "treeItemId", rootTreeItemId.get() - ); - var result = this.expandAllTreePathQueryRunner.run(variables); - List treeItemIdsToExpand = JsonPath.read(result, "$.data.viewer.editingContext.expandAllTreePath.treeItemIdsToExpand"); - assertThat(treeItemIdsToExpand).isNotEmpty(); - // only one superType tree item found underneath the Root entity. - assertThat(treeItemIdsToExpand.stream().filter(id -> id.startsWith(ExplorerDescriptionProvider.SETTING)).toList()).hasSize(1); - }; - StepVerifier.create(expandedTreeFlux) .consumeNextWith(initialExpandedTreeContentConsumer) - .then(getTreePathFromRoot) .thenCancel() .verify(Duration.ofSeconds(10)); } diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index 42d9ba1717..7c0c7a9ee1 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -35,9 +35,9 @@ import { } from '@eclipse-sirius/sirius-components-forms'; import { GanttRepresentation } from '@eclipse-sirius/sirius-components-gantt'; import { PortalRepresentation } from '@eclipse-sirius/sirius-components-portals'; -import { treeItemContextMenuEntryExtensionPoint } from '@eclipse-sirius/sirius-components-trees'; -import { ValidationView } from '@eclipse-sirius/sirius-components-validation'; import { SelectionDialog } from '@eclipse-sirius/sirius-components-selection'; +import { TreeRepresentation, treeItemContextMenuEntryExtensionPoint } from '@eclipse-sirius/sirius-components-trees'; +import { ValidationView } from '@eclipse-sirius/sirius-components-validation'; import { GQLReferenceWidget, ReferenceIcon, @@ -152,6 +152,7 @@ const representationFactories: RepresentationComponentFactory[] = [ (representationMetadata) => (getType(representationMetadata) === 'Gantt' ? GanttRepresentation : null), (representationMetadata) => (getType(representationMetadata) === 'Deck' ? DeckRepresentation : null), (representationMetadata) => (getType(representationMetadata) === 'Portal' ? PortalRepresentation : null), + (representationMetadata) => (getType(representationMetadata) === 'Tree' ? TreeRepresentation : null), ]; defaultExtensionRegistry.putData(representationFactoryExtensionPoint, { diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java new file mode 100644 index 0000000000..fccf2e2534 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.components.collaborative.api.IRepresentationConfiguration; +import org.eclipse.sirius.components.collaborative.api.IRepresentationEventProcessor; +import org.eclipse.sirius.components.collaborative.api.IRepresentationEventProcessorFactory; +import org.eclipse.sirius.components.collaborative.api.IRepresentationRefreshPolicyRegistry; +import org.eclipse.sirius.components.collaborative.api.IRepresentationSearchService; +import org.eclipse.sirius.components.collaborative.api.ISubscriptionManagerFactory; +import org.eclipse.sirius.components.collaborative.trees.api.ITreeEventHandler; +import org.eclipse.sirius.components.collaborative.trees.api.ITreeService; +import org.eclipse.sirius.components.collaborative.trees.api.TreeConfiguration; +import org.eclipse.sirius.components.collaborative.trees.api.TreeCreationParameters; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IObjectService; +import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +/** + * Used to create the tree event processors. + * + * @author Jerome Gout + */ +@Service +public class TreeEventProcessorFactory implements IRepresentationEventProcessorFactory { + + private final IRepresentationSearchService representationSearchService; + + private final IRepresentationDescriptionSearchService representationDescriptionSearchService; + + private final IObjectService objectService; + + private final ITreeService treeService; + + private final List treeEventHandlers; + + private final ISubscriptionManagerFactory subscriptionManagerFactory; + + private final IRepresentationRefreshPolicyRegistry representationRefreshPolicyRegistry; + + public TreeEventProcessorFactory(IRepresentationSearchService representationSearchService, IRepresentationDescriptionSearchService representationDescriptionSearchService, IObjectService objectService, ITreeService treeService, List treeEventHandlers, IRepresentationRefreshPolicyRegistry representationRefreshPolicyRegistry, ISubscriptionManagerFactory subscriptionManagerFactory) { + this.representationSearchService = Objects.requireNonNull(representationSearchService); + this.representationDescriptionSearchService = Objects.requireNonNull(representationDescriptionSearchService); + this.objectService = Objects.requireNonNull(objectService); + this.treeService = Objects.requireNonNull(treeService); + this.treeEventHandlers = Objects.requireNonNull(treeEventHandlers); + this.subscriptionManagerFactory = Objects.requireNonNull(subscriptionManagerFactory); + this.representationRefreshPolicyRegistry = Objects.requireNonNull(representationRefreshPolicyRegistry); + } + + @Override + public boolean canHandle(IRepresentationConfiguration configuration) { + return configuration instanceof TreeConfiguration; + } + + @Override + public Optional createRepresentationEventProcessor(IRepresentationConfiguration configuration, IEditingContext editingContext) { + if (configuration instanceof TreeConfiguration treeConfiguration) { + + Optional optionalTree = this.representationSearchService.findById(editingContext, this.getRepresentationId(configuration), Tree.class); + if (optionalTree.isPresent()) { + Tree tree = optionalTree.get(); + Optional optionalTreeDescription = this.representationDescriptionSearchService.findById(editingContext, tree.getDescriptionId()) + .filter(TreeDescription.class::isInstance) + .map(TreeDescription.class::cast); + Optional optionalObject = this.objectService.getObject(editingContext, tree.getTargetObjectId()); + if (optionalTreeDescription.isPresent() && optionalObject.isPresent()) { + TreeDescription treeDescription = optionalTreeDescription.get(); + Object object = optionalObject.get(); + + TreeCreationParameters treeCreationParameters = TreeCreationParameters.newTreeCreationParameters(treeConfiguration.getId()) + .treeDescription(treeDescription) + .activeFilterIds(List.of()) + .expanded(treeConfiguration.getExpanded()) + .editingContext(editingContext) + .targetObject(object) + .build(); + + IRepresentationEventProcessor treeEventProcessor = new TreeEventProcessor(editingContext, this.treeService, treeCreationParameters, this.treeEventHandlers, + this.subscriptionManagerFactory.create(), new SimpleMeterRegistry(), this.representationRefreshPolicyRegistry); + return Optional.of(treeEventProcessor); + } + } + } + return Optional.empty(); + } + + private String getRepresentationId(IRepresentationConfiguration configuration) { + return configuration.getId().substring(0, configuration.getId().indexOf('?')); + } + +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java index 1a056dec47..029c4bee3f 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java @@ -34,6 +34,7 @@ public Tree create(TreeCreationParameters treeCreationParameters) { VariableManager variableManager = new VariableManager(); variableManager.put(GetOrCreateRandomIdProvider.PREVIOUS_REPRESENTATION_ID, treeCreationParameters.getId()); variableManager.put(IEditingContext.EDITING_CONTEXT, treeCreationParameters.getEditingContext()); + variableManager.put(VariableManager.SELF, treeCreationParameters.getTargetObject()); variableManager.put(TreeRenderer.EXPANDED, treeCreationParameters.getExpanded()); variableManager.put(TreeRenderer.ACTIVE_FILTER_IDS, treeCreationParameters.getActiveFilterIds()); diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java new file mode 100644 index 0000000000..878b8ce248 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees.api; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.api.IRepresentationConfiguration; + +/** + * The configuration used to create a tree event processor. + * + * @author Jerome Gout + */ +public class TreeConfiguration implements IRepresentationConfiguration { + + private final String treeId; + + private final List expandedIds; + + public TreeConfiguration(String treeId, List expandedIds) { + this.expandedIds = Objects.requireNonNull(expandedIds); + StringBuilder idBuilder = new StringBuilder(treeId); + idBuilder.append("?"); + + List expandedObjectIds = expandedIds.stream().map(id -> URLEncoder.encode(id, StandardCharsets.UTF_8)).toList(); + idBuilder.append("expandedIds=[").append(String.join(",", expandedObjectIds)).append("]"); + + this.treeId = idBuilder.toString(); + } + + @Override + public String getId() { + return this.treeId; + } + + public List getExpanded() { + return this.expandedIds; + } +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java index 98b043316e..6bbeb59a81 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java @@ -37,6 +37,8 @@ public final class TreeCreationParameters { private IEditingContext editingContext; + private Object tragetObject; + private TreeCreationParameters() { // Prevent instantiation } @@ -61,6 +63,10 @@ public IEditingContext getEditingContext() { return this.editingContext; } + public Object getTargetObject() { + return this.tragetObject; + } + public static Builder newTreeCreationParameters(String id) { return new Builder(id); } @@ -88,6 +94,8 @@ public static final class Builder { private IEditingContext editingContext; + private Object targetObject; + private Builder(String id) { this.id = id; } @@ -112,6 +120,11 @@ public Builder editingContext(IEditingContext editingContext) { return this; } + public Builder targetObject(Object object) { + this.targetObject = object; + return this; + } + public TreeCreationParameters build() { TreeCreationParameters treeCreationParameters = new TreeCreationParameters(); treeCreationParameters.id = Objects.requireNonNull(this.id); @@ -119,6 +132,7 @@ public TreeCreationParameters build() { treeCreationParameters.activeFilterIds = Objects.requireNonNull(this.activeFilterIds); treeCreationParameters.expanded = Objects.requireNonNull(this.expanded); treeCreationParameters.editingContext = Objects.requireNonNull(this.editingContext); + treeCreationParameters.tragetObject = this.targetObject; // can be null; return treeCreationParameters; } } diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java new file mode 100644 index 0000000000..05f5f278e3 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees.dto; + +import java.util.List; +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +/** + * The input of the tree event subscription. + * + * @author Jerome Gout + */ +public record TreeEventInput(UUID id, String editingContextId, String treeId, List expanded) implements IInput { +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/CreateTreeEventHandler.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/CreateTreeEventHandler.java new file mode 100644 index 0000000000..c8508866f4 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/CreateTreeEventHandler.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees.handlers; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.collaborative.api.IRepresentationPersistenceService; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationSuccessPayload; +import org.eclipse.sirius.components.collaborative.messages.ICollaborativeMessageService; +import org.eclipse.sirius.components.collaborative.trees.services.TreeCreationService; +import org.eclipse.sirius.components.core.RepresentationMetadata; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IObjectService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * Handler used to create a new Tree representation. + * + * @author Jerome Gout + */ +@Service +public class CreateTreeEventHandler implements IEditingContextEventHandler { + + private final IRepresentationDescriptionSearchService representationDescriptionSearchService; + + private final IRepresentationPersistenceService representationPersistenceService; + + private final TreeCreationService treeCreationService; + + private final IObjectService objectService; + + private final ICollaborativeMessageService messageService; + + private final Counter counter; + + public CreateTreeEventHandler(IRepresentationDescriptionSearchService representationDescriptionSearchService, IRepresentationPersistenceService representationPersistenceService, + TreeCreationService treeCreationService, IObjectService objectService, ICollaborativeMessageService messageService, MeterRegistry meterRegistry) { + this.representationDescriptionSearchService = Objects.requireNonNull(representationDescriptionSearchService); + this.representationPersistenceService = Objects.requireNonNull(representationPersistenceService); + this.treeCreationService = Objects.requireNonNull(treeCreationService); + this.objectService = Objects.requireNonNull(objectService); + this.messageService = Objects.requireNonNull(messageService); + + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + if (input instanceof CreateRepresentationInput createRepresentationInput) { + return this.representationDescriptionSearchService.findById(editingContext, createRepresentationInput.representationDescriptionId()) + .filter(TreeDescription.class::isInstance) + .isPresent(); + } + return false; + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + this.counter.increment(); + + String message = this.messageService.invalidInput(input.getClass().getSimpleName(), CreateRepresentationInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(input.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + + if (input instanceof CreateRepresentationInput createRepresentationInput) { + Optional optionalTreeDescription = this.representationDescriptionSearchService.findById(editingContext, createRepresentationInput.representationDescriptionId()) + .filter(TreeDescription.class::isInstance) + .map(TreeDescription.class::cast); + Optional optionalObject = this.objectService.getObject(editingContext, createRepresentationInput.objectId()); + + if (optionalTreeDescription.isPresent() && optionalObject.isPresent()) { + TreeDescription treeDescription = optionalTreeDescription.get(); + Object object = optionalObject.get(); + + Tree tree = this.treeCreationService.create(createRepresentationInput.representationName(), object, treeDescription, editingContext); + + this.representationPersistenceService.save(editingContext, tree); + + var representationMetadata = new RepresentationMetadata(tree.getId(), tree.getKind(), tree.getLabel(), tree.getDescriptionId()); + payload = new CreateRepresentationSuccessPayload(input.id(), representationMetadata); + changeDescription = new ChangeDescription(ChangeKind.REPRESENTATION_CREATION, editingContext.getId(), input); + } + } + + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeCreationService.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeCreationService.java new file mode 100644 index 0000000000..d580d17865 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeCreationService.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees.services; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.representations.VariableManager; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.eclipse.sirius.components.trees.renderer.TreeRenderer; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +/** + * Service used to create tree representations. + * + * @author Jerome Gout + */ +@Service +public class TreeCreationService { + + private final Timer timer; + + public TreeCreationService(MeterRegistry meterRegistry) { + this.timer = Timer.builder(Monitoring.REPRESENTATION_EVENT_PROCESSOR_REFRESH).tag(Monitoring.NAME, "tree").register(meterRegistry); + } + + public Tree create(String label, Object targetObject, TreeDescription treeDescription, IEditingContext editingContext) { + return this.doRender(label, targetObject, editingContext, treeDescription); + } + + private Tree doRender(String label, Object targetObject, IEditingContext editingContext, TreeDescription treeDescription) { + long start = System.currentTimeMillis(); + + VariableManager variableManager = new VariableManager(); + variableManager.put(VariableManager.SELF, targetObject); + variableManager.put(IEditingContext.EDITING_CONTEXT, editingContext); + + Tree newTree = new TreeRenderer(variableManager, treeDescription).render(); + + long end = System.currentTimeMillis(); + this.timer.record(end - start, TimeUnit.MILLISECONDS); + return newTree; + } + +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeDeserializer.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeDeserializer.java new file mode 100644 index 0000000000..9bee723693 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/TreeDeserializer.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.collaborative.trees.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; + +import org.eclipse.sirius.components.collaborative.api.IRepresentationDeserializer; +import org.eclipse.sirius.components.representations.IRepresentation; +import org.eclipse.sirius.components.trees.Tree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * Used to deserialize a tree representation. + * + * @author Jerome Gout + */ +@Service +public class TreeDeserializer implements IRepresentationDeserializer { + + private final Logger logger = LoggerFactory.getLogger(TreeDeserializer.class); + + @Override + public boolean canHandle(ObjectNode root) { + return Optional.ofNullable(root.get("kind")) + .map(JsonNode::asText) + .filter(Tree.KIND::equals) + .isPresent(); + } + + @Override + public Optional handle(ObjectMapper mapper, ObjectNode root) { + try { + return Optional.of(mapper.readValue(root.toString(), Tree.class)); + } catch (JsonProcessingException exception) { + this.logger.warn(exception.getMessage(), exception); + } + + return Optional.empty(); + } +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls index d231858d23..5afbbf69ba 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls @@ -1,3 +1,14 @@ +extend type Subscription { + treeEvent(input: TreeEventInput!): TreeEventPayload! +} + +input TreeEventInput { + id: ID! + treeId: String! + editingContextId: ID! + expanded: [String!]! +} + extend type EditingContext { treePath(treeId: ID!, selectionEntryIds: [ID!]!): TreePath! expandAllTreePath(treeId: ID!, treeItemId: ID!): TreePath! diff --git a/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/subscription/SubscriptionTreeEventDataFetcher.java b/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/subscription/SubscriptionTreeEventDataFetcher.java new file mode 100644 index 0000000000..c5e93d9fc2 --- /dev/null +++ b/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/subscription/SubscriptionTreeEventDataFetcher.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.trees.graphql.datafetchers.subscription; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.sirius.components.annotations.spring.graphql.SubscriptionDataFetcher; +import org.eclipse.sirius.components.collaborative.trees.api.TreeConfiguration; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEventProcessorSubscriptionProvider; +import org.eclipse.sirius.components.graphql.api.IExceptionWrapper; +import org.eclipse.sirius.components.graphql.api.LocalContextConstants; +import org.reactivestreams.Publisher; + +import graphql.execution.DataFetcherResult; +import graphql.schema.DataFetchingEnvironment; + +/** + * The data fetcher used to send the refreshed tree to a subscription. + * + * @author Jerome Gout + */ +@SubscriptionDataFetcher(type = "Subscription", field = "treeEvent") +public class SubscriptionTreeEventDataFetcher implements IDataFetcherWithFieldCoordinates>> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IExceptionWrapper exceptionWrapper; + + private final IEventProcessorSubscriptionProvider eventProcessorSubscriptionProvider; + + public SubscriptionTreeEventDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEventProcessorSubscriptionProvider eventProcessorSubscriptionProvider) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper); + this.eventProcessorSubscriptionProvider = Objects.requireNonNull(eventProcessorSubscriptionProvider); + } + + @Override + public Publisher> get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, TreeEventInput.class); + var treeConfiguration = new TreeConfiguration(input.treeId(), input.expanded()); + + Map localContext = new HashMap<>(); + localContext.put(LocalContextConstants.EDITING_CONTEXT_ID, input.editingContextId()); + localContext.put(LocalContextConstants.REPRESENTATION_ID, treeConfiguration.getId()); + + return this.exceptionWrapper.wrapFlux(() -> this.eventProcessorSubscriptionProvider.getSubscription(input.editingContextId(), treeConfiguration, input), input) + .map(payload -> DataFetcherResult.newResult() + .data(payload) + .localContext(localContext) + .build()); + } +} diff --git a/packages/trees/frontend/sirius-components-trees/src/index.ts b/packages/trees/frontend/sirius-components-trees/src/index.ts index 19fe55a5f6..11aa353bf1 100644 --- a/packages/trees/frontend/sirius-components-trees/src/index.ts +++ b/packages/trees/frontend/sirius-components-trees/src/index.ts @@ -22,6 +22,7 @@ export * from './treeitems/TreeItemContextMenu.types'; export * from './treeitems/TreeItemContextMenuEntry.types'; export * from './treeitems/TreeItemContextMenuEntryExtensionPoints'; export * from './trees/FilterBar'; +export * from './trees/TreeRepresentation'; export * from './views/getTreeEventSubscription'; export * from './views/TreeFiltersMenu'; export * from './views/TreeFiltersMenu.types'; diff --git a/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.tsx b/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.tsx new file mode 100644 index 0000000000..40b84d41fb --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.tsx @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { RepresentationComponentProps } from '@eclipse-sirius/sirius-components-core'; +import { useState } from 'react'; +import { TreeView } from '../views/TreeView'; +import { TreeRepresentationState } from './TreeRepresentation.types'; +import { useTreeSubscription } from './useTreeSubscription'; + +export const TreeRepresentation = ({ editingContextId, representationId, readOnly }: RepresentationComponentProps) => { + const [state, setState] = useState({ + expanded: [], + maxDepth: 1, + }); + const { tree } = useTreeSubscription(editingContextId, representationId, state.expanded, state.maxDepth); + + const onExpandedElementChange = (expanded: string[], maxDepth: number) => { + setState((prevState) => ({ ...prevState, expanded, maxDepth })); + }; + + return ( +
+ +
+ ); +}; diff --git a/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.types.ts b/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.types.ts new file mode 100644 index 0000000000..74978ce1bc --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/trees/TreeRepresentation.types.ts @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +export interface TreeRepresentationState { + expanded: string[]; + maxDepth: number; +} diff --git a/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.tsx b/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.tsx new file mode 100644 index 0000000000..886b0fe903 --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.tsx @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { gql, OnDataOptions, useSubscription } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; + +import { useEffect, useState } from 'react'; + +import { getTreeEventSubscription } from '../views/getTreeEventSubscription'; +import { + GQLTreeEventData, + GQLTreeEventInput, + GQLTreeEventPayload, + GQLTreeEventVariables, + GQLTreeRefreshedEventPayload, + UseTreeSubscriptionState, + UseTreeSubscriptionValue, +} from './useTreeSubscription.types'; + +const isTreeRefreshedEventPayload = (payload: GQLTreeEventPayload): payload is GQLTreeRefreshedEventPayload => + payload.__typename === 'TreeRefreshedEventPayload'; + +export const useTreeSubscription = ( + editingContextId: string, + treeId: string, + expanded: string[], + maxDepth: number +): UseTreeSubscriptionValue => { + const [state, setState] = useState({ + id: crypto.randomUUID(), + tree: null, + complete: false, + }); + + const input: GQLTreeEventInput = { + id: state.id, + editingContextId, + treeId, + expanded, + }; + + const variables: GQLTreeEventVariables = { input }; + + const onData = ({ data }: OnDataOptions) => { + const { data: gqlTreeData } = data; + if (gqlTreeData) { + const { treeEvent: payload } = gqlTreeData; + if (isTreeRefreshedEventPayload(payload)) { + const { tree } = payload; + setState((prevState) => ({ ...prevState, tree })); + } + } + }; + + const onComplete = () => setState((prevState) => ({ ...prevState, complete: true })); + + const { error, loading } = useSubscription( + gql(getTreeEventSubscription(maxDepth, 'treeEvent', 'TreeEventInput')), + { + variables, + fetchPolicy: 'no-cache', + onData, + onComplete, + } + ); + + const { addErrorMessage } = useMultiToast(); + useEffect(() => { + if (error) { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + } + }, [error]); + + return { + loading, + tree: state.tree, + complete: state.complete, + }; +}; diff --git a/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.types.ts b/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.types.ts new file mode 100644 index 0000000000..451ea94b91 --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/trees/useTreeSubscription.types.ts @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { GQLTree } from '@eclipse-sirius/sirius-components-trees'; + +export interface UseTreeSubscriptionValue { + loading: boolean; + tree: GQLTree | null; + complete: boolean; +} + +export interface UseTreeSubscriptionState { + id: string; + tree: GQLTree | null; + complete: boolean; +} + +export interface GQLTreeEventInput { + id: string; + treeId: string; + editingContextId: string; + expanded: string[]; +} + +export interface GQLTreeEventVariables { + input: GQLTreeEventInput; +} + +export interface GQLTreeEventData { + treeEvent: GQLTreeEventPayload; +} + +export interface GQLTreeEventPayload { + __typename: string; +} + +export interface GQLTreeRefreshedEventPayload extends GQLTreeEventPayload { + id: string; + tree: GQLTree; +}