diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0bcca80849..996c847360 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -54,6 +54,7 @@ More existing APIs will be migrated to this new common pattern. - https://github.com/eclipse-sirius/sirius-web/issues/3575[#3575] [core] Restore support for studio palette colors - https://github.com/eclipse-sirius/sirius-web/issues/3582[#3582] [tree] Restore the initial direct edit tree item label for the explorer - https://github.com/eclipse-sirius/sirius-web/issues/3611[#3611] [diagram] Fix missing creation tool image in the contextual palette +- https://github.com/eclipse-sirius/sirius-web/issues/3628[#3628] [sirius-web] Restore support for expand all and reveal in the explorer === New Features diff --git a/packages/emf/backend/sirius-components-emf-forms/src/main/java/org/eclipse/sirius/components/emf/forms/EMFFormDescriptionProvider.java b/packages/emf/backend/sirius-components-emf-forms/src/main/java/org/eclipse/sirius/components/emf/forms/EMFFormDescriptionProvider.java index 099de47f32..c0d0944d7c 100644 --- a/packages/emf/backend/sirius-components-emf-forms/src/main/java/org/eclipse/sirius/components/emf/forms/EMFFormDescriptionProvider.java +++ b/packages/emf/backend/sirius-components-emf-forms/src/main/java/org/eclipse/sirius/components/emf/forms/EMFFormDescriptionProvider.java @@ -187,7 +187,7 @@ private GroupDescription getGroupDescription() { EcorePackage.Literals.ELONG_OBJECT, EcorePackage.Literals.ESHORT, EcorePackage.Literals.ESHORT_OBJECT - ); + ); for (var dataType : numericDataTypes) { ifDescriptions.add(new NumberIfDescriptionProvider(dataType, this.composedAdapterFactory, this.propertiesValidationProvider, this.emfMessageService, this.semanticTargetIdProvider).getIfDescription()); 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 new file mode 100644 index 0000000000..64fd09cf8a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExpandAllTreePathProvider.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * 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.explorer.services; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +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.IObjectSearchService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.trees.Tree; +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; + +/** + * Implementation of {@link IExpandAllTreePathProvider} for Sirius Web Tree. + * + * @author arichard + */ +@Service +public class ExpandAllTreePathProvider implements IExpandAllTreePathProvider { + + private final IObjectSearchService objectSearchService; + + private final IIdentityService identityService; + + private final IContentService contentService; + + private final IExplorerNavigationService explorerNavigationService; + + private final IRepresentationDataSearchService representationDataSearchService; + + public ExpandAllTreePathProvider(IObjectSearchService objectSearchService, IIdentityService identityService, IContentService contentService, IExplorerNavigationService explorerNavigationService, IRepresentationDataSearchService representationDataSearchService) { + this.objectSearchService = Objects.requireNonNull(objectSearchService); + this.identityService = Objects.requireNonNull(identityService); + this.contentService = Objects.requireNonNull(contentService); + this.explorerNavigationService = Objects.requireNonNull(explorerNavigationService); + this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + } + + @Override + public boolean canHandle(Tree tree) { + return tree.getDescriptionId().equals(ExplorerDescriptionProvider.DESCRIPTION_ID); + } + + @Override + public IPayload handle(IEditingContext editingContext, Tree tree, ExpandAllTreePathInput input) { + int maxDepth = 0; + String treeItemId = input.treeItemId(); + Set treeItemIdsToExpand = new HashSet<>(); + var object = this.objectSearchService.getObject(editingContext, treeItemId); + if (object.isPresent()) { + // We need to get the current depth of the tree item + var itemAncestors = this.explorerNavigationService.getAncestors(editingContext, treeItemId); + maxDepth = itemAncestors.size(); + maxDepth = this.addAllContents(editingContext, treeItemId, maxDepth, treeItemIdsToExpand); + } else { + // The object may be a document + var optionalEditingDomain = Optional.of(editingContext).filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast) + .map(IEMFEditingContext::getDomain); + + if (optionalEditingDomain.isPresent()) { + var optionalResource = optionalEditingDomain.get().getResourceSet().getResources().stream() + .filter(resource -> treeItemId.equals(resource.getURI().path().substring(1))) + .findFirst(); + if (optionalResource.isPresent()) { + var contents = optionalResource.get().getContents(); + if (!contents.isEmpty()) { + treeItemIdsToExpand.add(treeItemId); + for (var rootObject : contents) { + var rootObjectId = this.identityService.getId(rootObject); + var rootObjectTreePathMaxDepth = 1; + rootObjectTreePathMaxDepth = this.addAllContents(editingContext, rootObjectId, rootObjectTreePathMaxDepth, treeItemIdsToExpand); + maxDepth = Math.max(maxDepth, rootObjectTreePathMaxDepth); + } + } + } + } + } + return new ExpandAllTreePathSuccessPayload(input.id(), new TreePath(treeItemIdsToExpand.stream().toList(), maxDepth)); + } + + private int addAllContents(IEditingContext editingContext, String treeItemId, int depth, Set treeItemIdsToExpand) { + var depthConsidered = depth; + Optional optionalObject = this.objectSearchService.getObject(editingContext, treeItemId); + if (optionalObject.isPresent()) { + var contents = this.contentService.getContents(optionalObject.get()); + 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); + depthConsidered = Math.max(depthConsidered, childTreePathMaxDepth); + } + } else if (this.representationDataSearchService.existAnyRepresentationForTargetObjectId(treeItemId)) { + treeItemIdsToExpand.add(treeItemId); + depthConsidered = Math.max(depthConsidered, depth + 1); + } + } + + return depthConsidered; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerNavigationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerNavigationService.java new file mode 100644 index 0000000000..3fc1682ef0 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerNavigationService.java @@ -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 + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.explorer.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerNavigationService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationData; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataSearchService; +import org.springframework.stereotype.Service; + +/** + * Services for the navigation through the Sirius Web Explorer. + * + * @author arichard + */ +@Service +public class ExplorerNavigationService implements IExplorerNavigationService { + + private final IIdentityService identityService; + + private final IObjectSearchService objectSearchService; + + private final IRepresentationDataSearchService representationDataSearchService; + + public ExplorerNavigationService(IIdentityService identityService, IObjectSearchService objectSearchService, IRepresentationDataSearchService representationDataSearchService) { + this.identityService = Objects.requireNonNull(identityService); + this.objectSearchService = Objects.requireNonNull(objectSearchService); + this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + } + + @Override + public List getAncestors(IEditingContext editingContext, String treeItemId) { + List ancestorsIds = new ArrayList<>(); + + var optionalRepresentation = new UUIDParser().parse(treeItemId).flatMap(this.representationDataSearchService::findById); + var optionalSemanticObject = this.objectSearchService.getObject(editingContext, treeItemId); + + Optional optionalObject = Optional.empty(); + if (optionalRepresentation.isPresent()) { + // The first parent of a representation item is the item for its targetObject. + optionalObject = optionalRepresentation.map(RepresentationData::getTargetObjectId) + .flatMap(objectId -> this.objectSearchService.getObject(editingContext, objectId)); + } else if (optionalSemanticObject.isPresent()) { + // The first parent of a semantic object item is the item for its actual container + optionalObject = optionalSemanticObject.filter(EObject.class::isInstance) + .map(EObject.class::cast) + .map(eObject -> Optional. ofNullable(eObject.eContainer()).orElse(eObject.eResource())); + } + + while (optionalObject.isPresent()) { + ancestorsIds.add(this.getItemId(optionalObject.get())); + optionalObject = optionalObject + .filter(EObject.class::isInstance) + .map(EObject.class::cast) + .map(eObject -> Optional.ofNullable(eObject.eContainer()).orElse(eObject.eResource())); + } + return ancestorsIds; + } + + private String getItemId(Object object) { + String result = null; + if (object instanceof Resource resource) { + result = resource.getURI().path().substring(1); + } else if (object instanceof EObject) { + result = this.identityService.getId(object); + } + return result; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerTreePathProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerTreePathProvider.java new file mode 100644 index 0000000000..a6b9805a1d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerTreePathProvider.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.explorer.services; + +import java.util.HashSet; +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 Sirius Web Explorer. + * + * @author pcdavid + */ +@Service +public class ExplorerTreePathProvider implements ITreePathProvider { + + private final IExplorerNavigationService explorerNavigationService; + + public ExplorerTreePathProvider(IExplorerNavigationService explorerNavigationService) { + this.explorerNavigationService = Objects.requireNonNull(explorerNavigationService); + } + + @Override + public boolean canHandle(Tree tree) { + return tree.getDescriptionId().equals(ExplorerDescriptionProvider.DESCRIPTION_ID); + } + + @Override + public IPayload handle(IEditingContext editingContext, Tree tree, TreePathInput input) { + int maxDepth = 0; + Set allAncestors = new HashSet<>(); + for (String selectionEntryId : input.selectionEntryIds()) { + List itemAncestors = this.explorerNavigationService.getAncestors(editingContext, 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/explorer/services/api/IExplorerNavigationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/api/IExplorerNavigationService.java new file mode 100644 index 0000000000..ec1cd320c9 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/api/IExplorerNavigationService.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.explorer.services.api; + +import java.util.List; + +import org.eclipse.sirius.components.core.api.IEditingContext; + +/** + * Interface of the service for the navigation through the Sirius Web Explorer. + * + * @author arichard + */ +public interface IExplorerNavigationService { + + List getAncestors(IEditingContext editingContext, String treeItemId); + + /** + * Implementation which does nothing, used for mocks in unit tests. + * + * @author frouene + */ + class NoOp implements IExplorerNavigationService { + + @Override + public List getAncestors(IEditingContext editingContext, String treeItemId) { + return List.of(); + } + } + +} \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerExpandAllControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerExpandAllControllerTests.java new file mode 100644 index 0000000000..2df6041745 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerExpandAllControllerTests.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * 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.controllers.trees; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.jayway.jsonpath.JsonPath; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeRefreshedEventPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionRunner; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.tests.graphql.ExpandAllTreePathQueryRunner; +import org.eclipse.sirius.components.trees.tests.graphql.TreeEventSubscriptionRunner; +import org.eclipse.sirius.components.view.diagram.DiagramDescription; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; +import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.services.PapayaViewInjector; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +import graphql.execution.DataFetcherResult; +import reactor.test.StepVerifier; + +/** + * Integration tests of the computation of the expand all tree path in the explorer. + * + * @author sbegaudeau + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ExplorerExpandAllControllerTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private TreeEventSubscriptionRunner treeEventSubscriptionRunner; + + @Autowired + private ExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner; + + @Autowired + private IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry; + + @Autowired + private PapayaViewInjector papayaViewInjector; + + @Autowired + private ExpandAllTreePathQueryRunner expandAllTreePathQueryRunner; + + @Autowired + private IIdentityService identityService; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a studio, when we ask for the tree path to expand an object, then its path in the explorer is returned") + @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 givenStudioWhenWeAskForTheTreePathOfAnObjectThenItsPathInTheExplorerIsReturned() { + var input = new TreeEventInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), ExplorerDescriptionProvider.PREFIX, List.of(), List.of()); + var flux = this.treeEventSubscriptionRunner.run(input); + + var treeId = new AtomicReference(); + var objectId = new AtomicReference(); + + Consumer initialTreeContentConsumer = this.getTreeSubscriptionConsumer(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren()).hasSize(1); + assertThat(tree.getChildren()).allSatisfy(treeItem -> assertThat(treeItem.getChildren()).isEmpty()); + + treeId.set(tree.getId()); + }); + + Runnable createView = () -> { + var createViewInput = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), this.papayaViewInjector); + this.executeEditingContextFunctionRunner.execute(createViewInput).block(); + + Function getObjectIdFunction = editingContext -> Optional.of(editingContext) + .filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast) + .flatMap(emfEditingContext -> { + var iterator = emfEditingContext.getDomain().getResourceSet().getAllContents(); + var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); + return stream.filter(DiagramDescription.class::isInstance) + .map(DiagramDescription.class::cast) + .findFirst() + .map(EObject::eResource); + }) + .filter(Resource.class::isInstance) + .map(Resource.class::cast) + .map(resource -> resource.getURI().path().substring(1)) + .orElse(""); + + var getObjectIdInput = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), getObjectIdFunction); + var payload = this.executeEditingContextFunctionRunner.execute(getObjectIdInput).block(); + objectId.set(payload.result().toString()); + + var optionalEditingContextEventProcessor = this.editingContextEventProcessorRegistry.getEditingContextEventProcessors().stream() + .filter(editingContextEventProcessor -> editingContextEventProcessor.getEditingContextId().equals(StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString())) + .findFirst(); + + optionalEditingContextEventProcessor.ifPresentOrElse(editingContextEventProcessor -> { + editingContextEventProcessor.getRepresentationEventProcessors() + .forEach(representationEventProcessor -> representationEventProcessor.refresh(new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), createViewInput))); + }, () -> fail("Missing editing context event processor")); + }; + + Consumer updatedTreeContentConsumer = object -> this.getTreeSubscriptionConsumer(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren()).hasSize(2); + assertThat(tree.getChildren()).allSatisfy(treeItem -> assertThat(treeItem.getChildren()).isEmpty()); + }); + + var treeItemIds = new AtomicReference>(); + + Runnable getTreePath = () -> { + Map variables = Map.of( + "editingContextId", StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), + "treeId", treeId.get(), + "treeItemId", objectId.get() + ); + var result = this.expandAllTreePathQueryRunner.run(variables); + List treeItemIdsToExpand = JsonPath.read(result, "$.data.viewer.editingContext.expandAllTreePath.treeItemIdsToExpand"); + assertThat(treeItemIdsToExpand).isNotEmpty(); + + treeItemIds.set(treeItemIdsToExpand); + }; + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(createView) + .consumeNextWith(updatedTreeContentConsumer) + .then(getTreePath) + .thenCancel() + .verify(Duration.ofSeconds(10)); + + var expandedTreeInput = new TreeEventInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), ExplorerDescriptionProvider.PREFIX, treeItemIds.get(), List.of()); + var expandedTreeFlux = this.treeEventSubscriptionRunner.run(expandedTreeInput); + + Consumer initialExpandedTreeContentConsumer = this.getTreeSubscriptionConsumer(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren()).hasSize(2); + assertThat(tree.getChildren()).anySatisfy(treeItem -> assertThat(treeItem.getChildren()).isNotEmpty()); + + treeId.set(tree.getId()); + }); + + StepVerifier.create(expandedTreeFlux) + .consumeNextWith(initialExpandedTreeContentConsumer) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + + private Consumer getTreeSubscriptionConsumer(Consumer treeConsumer) { + return object -> Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(treeConsumer, () -> fail("Missing tree")); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreeFilterControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreeFilterControllerTests.java index f2831bcdd6..85b6405138 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreeFilterControllerTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreeFilterControllerTests.java @@ -34,6 +34,8 @@ import org.eclipse.sirius.web.application.studio.services.StudioExplorerTreeFilterProvider; import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -54,12 +56,20 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ExplorerTreeFilterControllerTests extends AbstractIntegrationTests { + @Autowired + private IGivenInitialServerState givenInitialServerState; + @Autowired private TreeEventSubscriptionRunner treeEventSubscriptionRunner; @Autowired private TreeFiltersQueryRunner treeFiltersQueryRunner; + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + @Test @DisplayName("Given a tree id, when we request its tree filters, then the list is returned") @Sql(scripts = {"/scripts/studio.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreePathControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreePathControllerTests.java new file mode 100644 index 0000000000..11a8f0affe --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerTreePathControllerTests.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * 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.controllers.trees; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.jayway.jsonpath.JsonPath; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeEventInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeRefreshedEventPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionRunner; +import org.eclipse.sirius.components.trees.tests.graphql.TreeEventSubscriptionRunner; +import org.eclipse.sirius.components.trees.tests.graphql.TreePathQueryRunner; +import org.eclipse.sirius.components.view.diagram.RectangularNodeStyleDescription; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; +import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.services.PapayaViewInjector; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +import graphql.execution.DataFetcherResult; +import reactor.test.StepVerifier; + +/** + * Integration tests of the computation of the tree path in the explorer. + * + * @author sbegaudeau + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ExplorerTreePathControllerTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private TreeEventSubscriptionRunner treeEventSubscriptionRunner; + + @Autowired + private ExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner; + + @Autowired + private IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry; + + @Autowired + private PapayaViewInjector papayaViewInjector; + + @Autowired + private TreePathQueryRunner treePathQueryRunner; + + @Autowired + private IIdentityService identityService; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a studio, when we ask for the tree path of an object, then its path in the explorer is returned") + @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 givenStudioWhenWeAskForTheTreePathOfAnObjectThenItsPathInTheExplorerIsReturned() { + var input = new TreeEventInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), ExplorerDescriptionProvider.PREFIX, List.of(), List.of()); + var flux = this.treeEventSubscriptionRunner.run(input); + + var treeId = new AtomicReference(); + var objectId = new AtomicReference(); + + Consumer initialTreeContentConsumer = object -> Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }, () -> fail("Missing tree")); + + Runnable createView = () -> { + var createViewInput = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), this.papayaViewInjector); + this.executeEditingContextFunctionRunner.execute(createViewInput).block(); + + Function getObjectIdFunction = editingContext -> Optional.of(editingContext) + .filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast) + .flatMap(emfEditingContext -> { + var iterator = emfEditingContext.getDomain().getResourceSet().getAllContents(); + var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); + return stream.filter(RectangularNodeStyleDescription.class::isInstance) + .findFirst(); + }) + .map(this.identityService::getId) + .orElse(""); + + var getObjectIdInput = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), getObjectIdFunction); + var payload = this.executeEditingContextFunctionRunner.execute(getObjectIdInput).block(); + objectId.set(payload.result().toString()); + + var optionalEditingContextEventProcessor = this.editingContextEventProcessorRegistry.getEditingContextEventProcessors().stream() + .filter(editingContextEventProcessor -> editingContextEventProcessor.getEditingContextId().equals(StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString())) + .findFirst(); + + optionalEditingContextEventProcessor.ifPresentOrElse(editingContextEventProcessor -> { + editingContextEventProcessor.getRepresentationEventProcessors() + .forEach(representationEventProcessor -> representationEventProcessor.refresh(new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), createViewInput))); + }, () -> fail("Missing editing context event processor")); + }; + + Consumer updatedTreeContentConsumer = object -> Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + }, () -> fail("Missing tree")); + + Runnable getTreePath = () -> { + Map variables = Map.of( + "editingContextId", StudioIdentifiers.EMPTY_STUDIO_PROJECT.toString(), + "treeId", treeId.get(), + "selectionEntryIds", List.of(objectId.get()) + ); + var result = this.treePathQueryRunner.run(variables); + List treeItemIdsToExpand = JsonPath.read(result, "$.data.viewer.editingContext.treePath.treeItemIdsToExpand"); + assertThat(treeItemIdsToExpand).isNotEmpty(); + }; + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(createView) + .consumeNextWith(updatedTreeContentConsumer) + .then(getTreePath) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/PapayaViewInjector.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/PapayaViewInjector.java new file mode 100644 index 0000000000..535f637f5f --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/PapayaViewInjector.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * 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.services; + +import java.util.Objects; +import java.util.function.Function; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.web.papaya.services.PapayaViewProvider; +import org.springframework.stereotype.Service; + +/** + * Used to inject the view from papaya into an editing context. + * + * @author sbegaudeau + */ +@Service +public class PapayaViewInjector implements Function { + + private final PapayaViewProvider papayaViewProvider; + + public PapayaViewInjector(PapayaViewProvider papayaViewProvider) { + this.papayaViewProvider = Objects.requireNonNull(papayaViewProvider); + } + + @Override + public Object apply(IEditingContext editingContext) { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + var view = this.papayaViewProvider.create(); + emfEditingContext.getDomain().getResourceSet().getResources().add(view.eResource()); + + return true; + } + return false; + } +} diff --git a/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/ExpandAllTreePathQueryRunner.java b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/ExpandAllTreePathQueryRunner.java new file mode 100644 index 0000000000..c8f547ac2c --- /dev/null +++ b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/ExpandAllTreePathQueryRunner.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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.tests.graphql; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IQueryRunner; +import org.springframework.stereotype.Service; + +/** + * Used to get all the tree items to expand from another tree item. + * + * @author sbegaudeau + */ +@Service +public class ExpandAllTreePathQueryRunner implements IQueryRunner { + + private static final String EXPAND_ALL_TREE_PATH_QUERY = """ + query getExpandAllTreePath($editingContextId: ID!, $treeId: ID!, $treeItemId: ID!) { + viewer { + editingContext(editingContextId: $editingContextId) { + expandAllTreePath(treeId: $treeId, treeItemId: $treeItemId) { + treeItemIdsToExpand + maxDepth + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public ExpandAllTreePathQueryRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(Map variables) { + return this.graphQLRequestor.execute(EXPAND_ALL_TREE_PATH_QUERY, variables); + } +} diff --git a/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/TreePathQueryRunner.java b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/TreePathQueryRunner.java new file mode 100644 index 0000000000..d490a1c89b --- /dev/null +++ b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/TreePathQueryRunner.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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.tests.graphql; + +import java.util.Map; +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IQueryRunner; +import org.springframework.stereotype.Service; + +/** + * Used to get the tree path with the GraphQL API. + * + * @author sbegaudeau + */ +@Service +public class TreePathQueryRunner implements IQueryRunner { + + private static final String TREE_PATH_QUERY = """ + query getTreePath($editingContextId: ID!, $treeId: ID!, $selectionEntryIds: [ID!]!) { + viewer { + editingContext(editingContextId: $editingContextId) { + treePath(treeId: $treeId, selectionEntryIds: $selectionEntryIds) { + treeItemIdsToExpand + maxDepth + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public TreePathQueryRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(Map variables) { + return this.graphQLRequestor.execute(TREE_PATH_QUERY, variables); + } +}