From 68f773a5f1881fae5d0ce5934752e6d83d5aa905 Mon Sep 17 00:00:00 2001 From: Florian Barbin Date: Wed, 10 Jul 2024 16:00:21 +0200 Subject: [PATCH] [3763] Allow displaying the Selection Dialog candidates in a tree viewer Bug: https://github.com/eclipse-sirius/sirius-web/issues/3763 Signed-off-by: Florian Barbin --- CHANGELOG.adoc | 9 ++ .../components/core/api/IContentService.java | 7 + .../core/api/IContentServiceDelegate.java | 7 + .../core/api/IDefaultContentService.java | 7 + .../components/core/api/IObjectService.java | 7 + .../core/services/ComposedContentService.java | 18 ++- .../core/services/ObjectService.java | 5 + .../emf/services/DefaultContentService.java | 22 ++- .../emf/services/DefaultIdentityService.java | 2 + .../emf/services/DefaultLabelService.java | 17 +++ .../emf/services/EMFImagePathService.java | 2 +- .../src/main/resources/icons/Resource.svg | 8 + .../selection/SelectionEventProcessor.java | 2 +- .../main/resources/schema/selection.graphqls | 4 + .../sirius-components-selection/pom.xml | 5 + .../components/selection/Selection.java | 28 ++++ .../components/selection/SelectionObject.java | 28 ++++ .../description/SelectionDescription.java | 28 ++++ .../selection/renderer/SelectionNode.java | 51 +++++++ .../selection/renderer/SelectionRenderer.java | 94 ++++++++++-- .../src/Selection.types.ts | 24 --- .../src/SelectionDialog.tsx | 142 +++++++++++++++--- .../src/SelectionDialog.types.ts | 8 + .../src/SelectionDialogMachine.ts | 4 +- .../src/SelectionEvent.types.ts | 4 + .../web/sample/tests/DynamicWidgetsTests.java | 3 +- ...electionDialogDescriptionItemProvider.java | 29 ++++ .../src/main/resources/plugin.properties | 2 + .../view/diagram/DiagramPackage.java | 60 +++++++- .../diagram/SelectionDialogDescription.java | 52 +++++++ .../view/diagram/impl/DiagramPackageImpl.java | 26 ++++ .../impl/SelectionDialogDescriptionImpl.java | 116 +++++++++++++- .../src/main/resources/model/diagram.ecore | 2 + .../src/main/resources/model/diagram.genmodel | 2 + .../components/view/emf/ViewConverter.java | 22 ++- .../view/emf/view/DynamicDiagramsTests.java | 3 +- .../view/emf/view/DynamicFormsTests.java | 3 +- 37 files changed, 772 insertions(+), 81 deletions(-) create mode 100644 packages/emf/backend/sirius-components-emf/src/main/resources/icons/Resource.svg create mode 100644 packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionNode.java delete mode 100644 packages/selection/frontend/sirius-components-selection/src/Selection.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 1ffc8a06ade..143320f0275 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -39,6 +39,8 @@ More existing APIs will be migrated to this new common pattern. * The `SelectionDescription` type has been renamed into `SelectionDialogDescription` and inherit from `DialogDescription`. * The `InvokeSingleClickOnDiagramElementToolInput#selectedObjectId` has been replaced by a `variables: [ToolVariable!]!` to make it possible to provide any kind of variable to the backend context. + + ``` input InvokeSingleClickOnDiagramElementToolInput { id: ID! @@ -54,6 +56,12 @@ More existing APIs will be migrated to this new common pattern. * The `String` attribute `org.eclipse.sirius.components.collaborative.diagrams.dto.InvokeSingleClickOnDiagramElementToolInput#selectedObjectId` has been replaced by the attribute `variables` of type `List` +- https://github.com/eclipse-sirius/sirius-web/issues/3763[#3695] [selection] Selection Dialog: Make it possible to display semantic candidates in a tree viewer + + * `org.eclipse.sirius.components.core.api.IContentService#getParent(Object object)` has been added to allow retrieving the parent element of an object. + * `org.eclipse.sirius.components.core.api.IObjectService#getParent(Object object)` has been added to allow retrieving the parent element of an object. + * `org.eclipse.sirius.components.view.emf.ViewConverter` requires a new service of type `IURLParser`. + === Dependency update - https://github.com/eclipse-sirius/sirius-web/issues/3510[#3510] [releng] Switch to Spring Boot 3.2.5 @@ -125,6 +133,7 @@ image:doc/screenshots/diagramFilterView.png[Diagram Filter View, 70%] - https://github.com/eclipse-sirius/sirius-web/issues/3591[#3591] [sirius-web] Add EditProject subtitle extension point - https://github.com/eclipse-sirius/sirius-web/issues/3662[#3662] [sirius-web] Add an extension point to contribute props to ReactFlow diagram renderer, contributed props must be memoized - https://github.com/eclipse-sirius/sirius-web/issues/3695[#3695] [form] Add an extension point to contribute widget labels decorators +- https://github.com/eclipse-sirius/sirius-web/issues/3763[#3695] [selection] Selection Dialog: Make it possible to display semantic candidates in a tree viewer === Improvements diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentService.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentService.java index 70f3b60bb31..d44cbc13bcd 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentService.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentService.java @@ -22,6 +22,8 @@ public interface IContentService { List getContents(Object object); + Object getParent(Object object); + /** * Implementation which does nothing, used for mocks in unit tests. * @@ -33,5 +35,10 @@ class NoOp implements IContentService { public List getContents(Object object) { return List.of(); } + + @Override + public Object getParent(Object object) { + return null; + } } } diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentServiceDelegate.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentServiceDelegate.java index 82adb9f8195..fce6c1f2b7c 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentServiceDelegate.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IContentServiceDelegate.java @@ -22,8 +22,11 @@ public interface IContentServiceDelegate { boolean canHandle(Object object); + List getContents(Object object); + Object getParent(Object object); + /** * Implementation which does nothing, used for mocks in unit tests. * @@ -38,6 +41,10 @@ public boolean canHandle(Object object) { public List getContents(Object object) { return List.of(); } + @Override + public Object getParent(Object object) { + return null; + } } diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IDefaultContentService.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IDefaultContentService.java index 14dfa5bc0db..c1f6167a735 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IDefaultContentService.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IDefaultContentService.java @@ -22,6 +22,8 @@ public interface IDefaultContentService { List getContents(Object object); + Object getParent(Object object); + /** * Implementation which does nothing, used for mocks in unit tests. * @@ -34,5 +36,10 @@ public List getContents(Object object) { return List.of(); } + @Override + public Object getParent(Object object) { + return null; + } + } } diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IObjectService.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IObjectService.java index 0d329ec7194..44d3d414cc7 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IObjectService.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/api/IObjectService.java @@ -40,6 +40,8 @@ public interface IObjectService { Optional getObject(IEditingContext editingContext, String objectId); + Object getParent(Object object); + /** * Implementation which does nothing, used for mocks in unit tests. * @@ -91,6 +93,11 @@ public String getKind(Object object) { public Optional getObject(IEditingContext editingContext, String objectId) { return Optional.empty(); } + + @Override + public Object getParent(Object object) { + return null; + } } } diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ComposedContentService.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ComposedContentService.java index 6ed16feceb0..88407a2c4dc 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ComposedContentService.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ComposedContentService.java @@ -12,15 +12,14 @@ *******************************************************************************/ package org.eclipse.sirius.components.core.services; -import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Objects; import org.eclipse.sirius.components.core.api.IContentService; import org.eclipse.sirius.components.core.api.IContentServiceDelegate; import org.eclipse.sirius.components.core.api.IDefaultContentService; import org.eclipse.sirius.components.core.api.IIdentityService; - -import java.util.List; -import java.util.Objects; +import org.springframework.stereotype.Service; /** * Implementation of {@link IIdentityService} which delegates to {@link IContentServiceDelegate} or fallback to @@ -50,4 +49,15 @@ public List getContents(Object object) { } return this.defaultContentService.getContents(object); } + + @Override + public Object getParent(Object object) { + var optionalDelegate = this.contentServiceDelegate.stream() + .filter(delegate -> delegate.canHandle(object)) + .findFirst(); + if (optionalDelegate.isPresent()) { + return optionalDelegate.get().getParent(object); + } + return this.defaultContentService.getParent(object); + } } diff --git a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ObjectService.java b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ObjectService.java index 51e5281e0f9..535ec176a07 100644 --- a/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ObjectService.java +++ b/packages/core/backend/sirius-components-core/src/main/java/org/eclipse/sirius/components/core/services/ObjectService.java @@ -92,4 +92,9 @@ public Optional getObject(IEditingContext editingContext, String objectI return this.objectSearchService.getObject(editingContext, objectId); } + @Override + public Object getParent(Object object) { + return this.contentService.getParent(object); + } + } diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultContentService.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultContentService.java index a235bfa652d..71d0e201e98 100644 --- a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultContentService.java +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultContentService.java @@ -12,17 +12,16 @@ *******************************************************************************/ package org.eclipse.sirius.components.emf.services; -import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import org.eclipse.emf.common.notify.Adapter; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.edit.provider.ComposedAdapterFactory; import org.eclipse.emf.edit.provider.IEditingDomainItemProvider; import org.eclipse.sirius.components.core.api.IDefaultContentService; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import org.springframework.stereotype.Service; /** * Default implementation of {@link IDefaultContentService}. @@ -50,4 +49,17 @@ public List getContents(Object object) { } return contents; } + @Override + public Object getParent(Object object) { + Object parent = null; + if (object instanceof EObject eObject) { + Adapter adapter = this.composedAdapterFactory.adapt(eObject, IEditingDomainItemProvider.class); + if (adapter instanceof IEditingDomainItemProvider contentProvider) { + parent = contentProvider.getParent(eObject); + } else { + parent = eObject.eContainer(); + } + } + return parent; + } } diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultIdentityService.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultIdentityService.java index c87997e0d5b..38afaa5be66 100644 --- a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultIdentityService.java +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultIdentityService.java @@ -54,6 +54,8 @@ public String getId(Object object) { id = representation.getId(); } else if (object instanceof IEditingContext editingContext) { id = editingContext.getId(); + } else if (object instanceof Resource resource) { + id = resource.getURI().path().substring(1); } return id; } diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultLabelService.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultLabelService.java index b1b8351ce70..e6454addcfd 100644 --- a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultLabelService.java +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/DefaultLabelService.java @@ -24,12 +24,14 @@ import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.edit.provider.ComposedAdapterFactory; import org.eclipse.emf.edit.provider.ComposedImage; import org.eclipse.emf.edit.provider.IItemLabelProvider; import org.eclipse.emf.edit.provider.ReflectiveItemProvider; import org.eclipse.sirius.components.collaborative.api.IRepresentationImageProvider; import org.eclipse.sirius.components.core.api.IDefaultLabelService; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.representations.IRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,10 +68,20 @@ public String getLabel(Object object) { .orElse(""); } else if (object instanceof IRepresentation representation) { label = representation.getLabel(); + } else if (object instanceof Resource resource) { + label = this.getResourceLabel(resource); } return label; } + private String getResourceLabel(Resource resource) { + return resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast).findFirst() + .map(ResourceMetadataAdapter::getName) + .orElse(resource.getURI().lastSegment()); + } + @Override public String getFullLabel(Object object) { String fullLabel = ""; @@ -81,6 +93,8 @@ public String getFullLabel(Object object) { } } else if (object instanceof IRepresentation representation) { fullLabel = representation.getLabel(); + } else if (object instanceof Resource resource) { + fullLabel = this.getResourceLabel(resource); } else { fullLabel = this.getLabel(object); } @@ -138,6 +152,9 @@ public List getImagePath(Object object) { .flatMap(Optional::stream) .toList(); } + else if (object instanceof Resource) { + result = List.of("/icons/Resource.svg"); + } return result; } diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EMFImagePathService.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EMFImagePathService.java index 5e67d44aab6..3ed377987d6 100644 --- a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EMFImagePathService.java +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EMFImagePathService.java @@ -25,7 +25,7 @@ @Service public class EMFImagePathService implements IImagePathService { - private static final List IMAGES_PATHS = List.of("/icons/full/obj16", "/icons/full/ovr16"); + private static final List IMAGES_PATHS = List.of("/icons/full/obj16", "/icons/full/ovr16", "/icons"); @Override public List getPaths() { diff --git a/packages/emf/backend/sirius-components-emf/src/main/resources/icons/Resource.svg b/packages/emf/backend/sirius-components-emf/src/main/resources/icons/Resource.svg new file mode 100644 index 00000000000..1a76ef6435a --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/resources/icons/Resource.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/selection/backend/sirius-components-collaborative-selection/src/main/java/org/eclipse/sirius/components/collaborative/selection/SelectionEventProcessor.java b/packages/selection/backend/sirius-components-collaborative-selection/src/main/java/org/eclipse/sirius/components/collaborative/selection/SelectionEventProcessor.java index f59b64cae58..000a6064455 100644 --- a/packages/selection/backend/sirius-components-collaborative-selection/src/main/java/org/eclipse/sirius/components/collaborative/selection/SelectionEventProcessor.java +++ b/packages/selection/backend/sirius-components-collaborative-selection/src/main/java/org/eclipse/sirius/components/collaborative/selection/SelectionEventProcessor.java @@ -144,7 +144,7 @@ private Selection refreshSelection() { variableManager.put(IEditingContext.EDITING_CONTEXT, this.editingContext); variableManager.put(GetOrCreateRandomIdProvider.PREVIOUS_REPRESENTATION_ID, this.id); - Selection selection = new SelectionRenderer(variableManager, this.selectionDescription).render(); + Selection selection = new SelectionRenderer(variableManager, this.selectionDescription, this.objectService).render(); this.logger.trace("Selection refreshed: {}", selection.getId()); diff --git a/packages/selection/backend/sirius-components-collaborative-selection/src/main/resources/schema/selection.graphqls b/packages/selection/backend/sirius-components-collaborative-selection/src/main/resources/schema/selection.graphqls index 35ff9563065..35c315ff94d 100644 --- a/packages/selection/backend/sirius-components-collaborative-selection/src/main/resources/schema/selection.graphqls +++ b/packages/selection/backend/sirius-components-collaborative-selection/src/main/resources/schema/selection.graphqls @@ -22,12 +22,16 @@ type Selection implements Representation { targetObjectId: String! message: String objects: [SelectionObject!]! + displayedAsTree: Boolean! + expendedAtOpening: Boolean! } type SelectionObject { id: ID! label: String! iconURL: [String!]! + parentId: String + isSelectable: Boolean! } type SelectionDescription implements RepresentationDescription { diff --git a/packages/selection/backend/sirius-components-selection/pom.xml b/packages/selection/backend/sirius-components-selection/pom.xml index b41fa9ba4cb..74008bc53dd 100644 --- a/packages/selection/backend/sirius-components-selection/pom.xml +++ b/packages/selection/backend/sirius-components-selection/pom.xml @@ -50,6 +50,11 @@ sirius-components-representations 2024.5.6 + + org.eclipse.sirius + sirius-components-core + 2024.5.6 + org.eclipse.sirius sirius-components-tests diff --git a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/Selection.java b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/Selection.java index 269f2560b0e..c8eaa7ce16d 100644 --- a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/Selection.java +++ b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/Selection.java @@ -47,6 +47,10 @@ public final class Selection implements IRepresentation { private List objects; + private boolean displayedAsTree; + + private boolean expendedAtOpening; + private Selection() { // Prevent instantiation } @@ -80,6 +84,14 @@ public String getMessage() { return this.message; } + public boolean isDisplayedAsTree() { + return this.displayedAsTree; + } + + public boolean isExpendedAtOpening() { + return this.expendedAtOpening; + } + public List getObjects() { return this.objects; } @@ -115,6 +127,10 @@ public static final class Builder { private List objects; + private boolean displayedAsTree; + + private boolean expendedAtOpening; + private Builder(String id) { this.id = Objects.requireNonNull(id); } @@ -144,6 +160,16 @@ public Builder objects(List objects) { return this; } + public Builder displayedAsTree(boolean displayedAsTree) { + this.displayedAsTree = displayedAsTree; + return this; + } + + public Builder expendedAtOpening(boolean expendedAtOpening) { + this.expendedAtOpening = expendedAtOpening; + return this; + } + public Selection build() { Selection selection = new Selection(); selection.id = Objects.requireNonNull(this.id); @@ -153,6 +179,8 @@ public Selection build() { selection.targetObjectId = Objects.requireNonNull(this.targetObjectId); selection.message = this.message; selection.objects = Objects.requireNonNull(this.objects); + selection.displayedAsTree = this.displayedAsTree; + selection.expendedAtOpening = this.expendedAtOpening; return selection; } } diff --git a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/SelectionObject.java b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/SelectionObject.java index aa8b78d3b35..35f8265b77a 100644 --- a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/SelectionObject.java +++ b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/SelectionObject.java @@ -34,6 +34,10 @@ public final class SelectionObject { private List iconURL; + private String parentId; + + private boolean isSelectable; + private SelectionObject() { // Prevent instantiation } @@ -54,6 +58,14 @@ public List getIconURL() { return this.iconURL; } + public String getParentId() { + return this.parentId; + } + + public boolean isSelectable() { + return this.isSelectable; + } + @Override public String toString() { String pattern = "{0} '{'id: {1}, label: {2}, imageURL: {3}'}'"; @@ -74,6 +86,10 @@ public static final class Builder { private List iconURL; + private String parentId; + + private boolean isSelectable; + private Builder(UUID id) { this.id = Objects.requireNonNull(id); } @@ -88,11 +104,23 @@ public Builder iconURL(List iconURL) { return this; } + public Builder parentId(String parentId) { + this.parentId = Objects.requireNonNull(parentId); + return this; + } + + public Builder isSelectable(boolean isSelectable) { + this.isSelectable = isSelectable; + return this; + } + public SelectionObject build() { SelectionObject selectionObject = new SelectionObject(); selectionObject.id = Objects.requireNonNull(this.id); selectionObject.label = Objects.requireNonNull(this.label); selectionObject.iconURL = Objects.requireNonNull(this.iconURL); + selectionObject.parentId = this.parentId; // The parentId can be null (flat layout vs tree layout) + selectionObject.isSelectable = this.isSelectable; return selectionObject; } } diff --git a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/description/SelectionDescription.java b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/description/SelectionDescription.java index f1b2c8a4ab5..358b2f18b36 100644 --- a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/description/SelectionDescription.java +++ b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/description/SelectionDescription.java @@ -50,6 +50,10 @@ public final class SelectionDescription implements IRepresentationDescription { private Predicate canCreatePredicate; + private boolean displayedAsTree; + + private boolean expandedAtOpening; + private SelectionDescription() { // Prevent instantiation } @@ -101,6 +105,14 @@ public Predicate getCanCreatePredicate() { return this.canCreatePredicate; } + public boolean isDisplayedAsTree() { + return this.displayedAsTree; + } + + public boolean isExpandedAtOpening() { + return this.expandedAtOpening; + } + @Override public String toString() { String pattern = "{0} '{'id: {1}, label: {2}'}'"; @@ -135,6 +147,10 @@ public static final class Builder { private Predicate canCreatePredicate; + private boolean displayedAsTree; + + private boolean expandedAtOpening; + private Builder(String id) { this.id = Objects.requireNonNull(id); } @@ -184,6 +200,16 @@ public Builder canCreatePredicate(Predicate canCreatePredicate) return this; } + public Builder displayedAsTree(boolean displayedAsTree) { + this.displayedAsTree = displayedAsTree; + return this; + } + + public Builder expandedAtOpening(boolean expandedAtOpening) { + this.expandedAtOpening = expandedAtOpening; + return this; + } + public SelectionDescription build() { SelectionDescription selectionDescription = new SelectionDescription(); selectionDescription.id = Objects.requireNonNull(this.id); @@ -196,6 +222,8 @@ public SelectionDescription build() { selectionDescription.objectsProvider = Objects.requireNonNull(this.objectsProvider); selectionDescription.selectionObjectsIdProvider = Objects.requireNonNull(this.selectionObjectsIdProvider); selectionDescription.canCreatePredicate = Objects.requireNonNull(this.canCreatePredicate); + selectionDescription.displayedAsTree = this.displayedAsTree; + selectionDescription.expandedAtOpening = this.expandedAtOpening; return selectionDescription; } diff --git a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionNode.java b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionNode.java new file mode 100644 index 00000000000..28d23cb7e11 --- /dev/null +++ b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionNode.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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.selection.renderer; + +import java.util.Objects; + +/** + * Internal class to compute the tree representation. + * + * @author fbarbin + */ +public class SelectionNode { + + private Object object; + + private boolean isSelectable; + + private String parentId; + + public SelectionNode(Object object, boolean isSelectable) { + this.object = Objects.requireNonNull(object); + this.isSelectable = isSelectable; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public Object getObject() { + return this.object; + } + + public String getParentId() { + return this.parentId; + } + + public boolean isSelectable() { + return this.isSelectable; + } + +} diff --git a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionRenderer.java b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionRenderer.java index df62719d1be..c3ab0c0f5ea 100644 --- a/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionRenderer.java +++ b/packages/selection/backend/sirius-components-selection/src/main/java/org/eclipse/sirius/components/selection/renderer/SelectionRenderer.java @@ -13,10 +13,13 @@ package org.eclipse.sirius.components.selection.renderer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.UUID; +import org.eclipse.sirius.components.core.api.IObjectService; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.selection.Selection; import org.eclipse.sirius.components.selection.SelectionObject; @@ -33,9 +36,14 @@ public class SelectionRenderer { private final SelectionDescription selectionDescription; - public SelectionRenderer(VariableManager variableManager, SelectionDescription selectionDescription) { + private final Map computedNodes = new HashMap<>(); + + private final IObjectService objectService; + + public SelectionRenderer(VariableManager variableManager, SelectionDescription selectionDescription, IObjectService objectService) { this.variableManager = Objects.requireNonNull(variableManager); this.selectionDescription = Objects.requireNonNull(selectionDescription); + this.objectService = Objects.requireNonNull(objectService); } public Selection render() { @@ -45,32 +53,94 @@ public Selection render() { String targetObjectId = this.selectionDescription.getTargetObjectIdProvider().apply(this.variableManager); List selectionObjects = this.selectionDescription.getObjectsProvider().apply(this.variableManager); - List objects = new ArrayList<>(selectionObjects.size()); - for (Object selectionObject : selectionObjects) { - VariableManager selectionObjectVariableManager = this.variableManager.createChild(); - selectionObjectVariableManager.put(VariableManager.SELF, selectionObject); - objects.add(this.renderSelectionObject(selectionObjectVariableManager)); + List objects; + if (this.selectionDescription.isDisplayedAsTree()) { + objects = this.computeTreeSelectionObjects(selectionObjects); + } else { + objects = this.computeFlatListSelectionObject(selectionObjects); } - - // @formatter:off + boolean expandAtOpening = this.selectionDescription.isExpandedAtOpening(); + boolean displayedAsTree = this.selectionDescription.isDisplayedAsTree(); return Selection.newSelection(id) .descriptionId(this.selectionDescription.getId()) .label(label) .targetObjectId(targetObjectId) .message(message) .objects(objects) + .expendedAtOpening(expandAtOpening) + .displayedAsTree(displayedAsTree) .build(); - // @formatter:on } - private SelectionObject renderSelectionObject(VariableManager selectionObjectVariableManager) { + private List computeFlatListSelectionObject(List selectionObjects) { + List objects = new ArrayList<>(selectionObjects.size()); + for (Object selectionObject : selectionObjects) { + VariableManager selectionObjectVariableManager = this.variableManager.createChild(); + selectionObjectVariableManager.put(VariableManager.SELF, selectionObject); + objects.add(this.renderSelectionObject(selectionObjectVariableManager, null, true)); + } + return objects; + } + + private List computeTreeSelectionObjects(List selectionObjects) { + selectionObjects.forEach(element -> { + String elementId = this.objectService.getId(element); + // We make sure the element has not been computed yet (by the computeAncestors of a previous element for + // instance) + if (!this.computedNodes.containsKey(elementId)) { + SelectionNode elementSelectionNode = this.createSelectionNode(element, true); + this.computedNodes.put(elementId, elementSelectionNode); + this.computeAncestors(element, elementSelectionNode, selectionObjects); + } + }); + return this.computedNodes.keySet().stream() + .map(this::convertToSelectionObject) + .toList(); + } + + private void computeAncestors(Object element, SelectionNode elementSelectionNode, List selectionObjects) { + Object parent = this.objectService.getParent(element); + // If the parent is null, it a root element. + if (parent != null) { + String parentId = this.objectService.getId(parent); + // If the parentNode exists in the computedNodes, we add the element as child and we stop here (it has + // already be computed by a previous pass) + if (this.computedNodes.containsKey(parentId)) { + elementSelectionNode.setParentId(parentId); + } else { + // The parentNode has not been computed yet + boolean isSelectable = selectionObjects.contains(parent); + SelectionNode parentNode = this.createSelectionNode(parent, isSelectable); + this.computedNodes.put(parentId, parentNode); + elementSelectionNode.setParentId(parentId); + this.computeAncestors(parent, parentNode, selectionObjects); + } + } + } + + private SelectionNode createSelectionNode(Object element, boolean isSelectable) { + return new SelectionNode(element, isSelectable); + } + + private SelectionObject convertToSelectionObject(String id) { + SelectionNode selectionNode = this.computedNodes.get(id); + VariableManager selectionObjectVariableManager = this.variableManager.createChild(); + selectionObjectVariableManager.put(VariableManager.SELF, selectionNode.getObject()); + return this.renderSelectionObject(selectionObjectVariableManager, selectionNode.getParentId(), selectionNode.isSelectable()); + } + + private SelectionObject renderSelectionObject(VariableManager selectionObjectVariableManager, String parentId, boolean selectable) { String id = this.selectionDescription.getSelectionObjectsIdProvider().apply(selectionObjectVariableManager); String label = this.selectionDescription.getLabelProvider().apply(selectionObjectVariableManager); List iconURL = this.selectionDescription.getIconURLProvider().apply(selectionObjectVariableManager); - return SelectionObject.newSelectionObject(UUID.fromString(id)) + var selectionObjectBuilder = SelectionObject.newSelectionObject(UUID.fromString(id)) .label(label) .iconURL(iconURL) - .build(); + .isSelectable(selectable); + if (parentId != null) { + selectionObjectBuilder.parentId(parentId); + } + return selectionObjectBuilder.build(); } } diff --git a/packages/selection/frontend/sirius-components-selection/src/Selection.types.ts b/packages/selection/frontend/sirius-components-selection/src/Selection.types.ts deleted file mode 100644 index 8aed258b9d0..00000000000 --- a/packages/selection/frontend/sirius-components-selection/src/Selection.types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2021, 2023 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 Selection { - id: string; - message: string; - objects: SelectionObject[]; -} - -export interface SelectionObject { - id: string; - label: string; - iconURL: string[]; -} diff --git a/packages/selection/frontend/sirius-components-selection/src/SelectionDialog.tsx b/packages/selection/frontend/sirius-components-selection/src/SelectionDialog.tsx index 6789f51ca16..3591ac31001 100644 --- a/packages/selection/frontend/sirius-components-selection/src/SelectionDialog.tsx +++ b/packages/selection/frontend/sirius-components-selection/src/SelectionDialog.tsx @@ -11,8 +11,12 @@ * Obeo - initial API and implementation *******************************************************************************/ import { gql, useSubscription } from '@apollo/client'; + import { IconOverlay, Toast, useSelection } from '@eclipse-sirius/sirius-components-core'; import { DiagramDialogComponentProps, useDialog } from '@eclipse-sirius/sirius-components-diagrams'; + +import { theme } from '@eclipse-sirius/sirius-components-core'; + import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; @@ -23,10 +27,18 @@ import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import CropDinIcon from '@material-ui/icons/CropDin'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { TreeItem as MuiTreeItem } from '@material-ui/lab'; +import TreeView from '@material-ui/lab/TreeView'; import { useMachine } from '@xstate/react'; import { useEffect } from 'react'; + +import { TreeItemProps } from './SelectionDialog.types'; + import { HandleCompleteEvent, HandleSelectionUpdatedEvent, @@ -40,6 +52,57 @@ import { } from './SelectionDialogMachine'; import { GQLSelectionEventSubscription } from './SelectionEvent.types'; +const useTreeItemWidgetStyles = makeStyles((theme) => ({ + label: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + columnGap: theme.spacing(1), + }, + disabled: {}, +})); + +const treeItemLabelStyle = (theme: Theme, isSelectable: boolean): React.CSSProperties => { + const treeItemLabelStyle: React.CSSProperties = {}; + if (!isSelectable) { + treeItemLabelStyle.color = theme.palette.text.disabled; + } + return treeItemLabelStyle; +}; + +const TreeItem = ({ selectionObject, handleListItemClick, selectionObjects }: TreeItemProps) => { + const classes = useTreeItemWidgetStyles(); + + const handleClick: React.MouseEventHandler = () => { + if (selectionObject.isSelectable) { + handleListItemClick(selectionObject.id); + } + }; + + const label = ( +
+ + {selectionObject.label} +
+ ); + + const childObjects = selectionObjects.filter( + (currentSelectionObject) => currentSelectionObject.parentId === selectionObject.id + ); + return ( + + {childObjects.map((object) => ( + + ))} + + ); +}; + const selectionEventSubscription = gql` subscription selectionEvent($input: SelectionEventInput!) { selectionEvent(input: $input) { @@ -49,10 +112,14 @@ const selectionEventSubscription = gql` id targetObjectId message + displayedAsTree + expendedAtOpening objects { id label iconURL + isSelectable + parentId } } } @@ -128,6 +195,54 @@ export const SelectionDialog = ({ dialogDescriptionId, onClose, onFinish }: Diag dispatch({ type: 'HANDLE_SELECTION_UPDATED', selectedObjectId } as HandleSelectionUpdatedEvent); }; + let content: JSX.Element; + + if (selection?.displayedAsTree) { + const expandedNodesIds = selection?.expendedAtOpening ? selection?.objects.map((object) => object.id) : []; + const rootSelectionObjects = selection?.objects.filter((object) => !object.parentId); + content = ( + } + defaultExpanded={expandedNodesIds} + defaultExpandIcon={}> + {rootSelectionObjects.map((object) => ( + + ))} + + ); + } else { + content = ( + + {selection?.objects.map((selectionObject) => ( + handleListItemClick(selectionObject.id)} + data-testid={selectionObject.label}> + + {selectionObject.iconURL.length > 0 ? ( + + ) : ( + + )} + + + + ))} + + ); + } return ( <> Selection Dialog {selection?.message} - - {selection?.objects.map((selectionObject) => ( - handleListItemClick(selectionObject.id)} - data-testid={selectionObject.label}> - - {selectionObject.iconURL.length > 0 ? ( - - ) : ( - - )} - - - - ))} - + {content}