From b9aac8d09c66e72175c5b49aeec3606a959db0ef Mon Sep 17 00:00:00 2001 From: Pierre-Charles David Date: Mon, 21 Feb 2022 13:34:41 +0100 Subject: [PATCH] [1070] Reveal the currently selected representation in the explorer - Switch to a flat tree format for the GraphQL message so that the frontend does not need to keep track of try to guess the depth of the tree (and remove the now obsolete maxDpeth). - Ask the backend to reveal the currently selected elements by expanding their ancestors. - Update TreeRenderer to convert elements to reveal (TreeEventInput.revealed) into ancestors to expand using the new TreeDescription.getAncestorsProvider(). Bug: https://github.com/eclipse-sirius/sirius-components/issues/1070 Signed-off-by: Pierre-Charles David --- CHANGELOG.adoc | 3 + .../trees/TreeEventProcessorFactory.java | 1 + .../collaborative/trees/TreeService.java | 1 + .../trees/api/TreeConfiguration.java | 13 +- .../trees/api/TreeCreationParameters.java | 18 ++- .../trees/dto/TreeEventInput.java | 15 ++- .../src/main/resources/schema/tree.graphqls | 6 +- .../trees/description/TreeDescription.java | 14 ++ .../trees/renderer/TreeRenderer.java | 23 +++- .../explorer/ExplorerWebSocketContainer.tsx | 48 ++++++- .../GraphqlTreeEventSubscription.test.ts | 59 -------- .../src/explorer/__tests__/reducer.test.ts | 126 +++++++++++++++--- .../src/explorer/getTreeEventSubscription.ts | 56 -------- frontend/src/explorer/machine.ts | 3 + frontend/src/explorer/reducer.ts | 93 +++++++++++-- frontend/src/tree/TreeItem.tsx | 2 +- frontend/src/workbench/Workbench.tsx | 1 + 17 files changed, 323 insertions(+), 159 deletions(-) delete mode 100644 frontend/src/explorer/__tests__/GraphqlTreeEventSubscription.test.ts delete mode 100644 frontend/src/explorer/getTreeEventSubscription.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index ad7714aee21..605f193bb56 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -29,6 +29,8 @@ - https://github.com/eclipse-sirius/sirius-components/issues/1045[#1045] [core] Providers will now return List instead of List. This makes it possible for applications to reuse existing services to implement providers without making useless copies of lists - https://github.com/eclipse-sirius/sirius-components/issues/1068[#1068] [form] The form representation is now supporting multiple elements as an input - https://github.com/eclipse-sirius/sirius-components/issues/1068[#1068] [workbench] The integration of the details view in the workbench is not limited to semantic objects with a kind starting with `siriusComponents://semantic`. Any object can be used as the input of the details view and we will now provide the identifier of all the objects in the selection. This may include graphical elements such as nodes, edges, representations or anything selected in the explorer for example +- https://github.com/eclipse-sirius/sirius-components/issues/1070[#1070] [tree] `TreeDescription` has a new `getAncestorsProvider()` API used to compute the set of ancestors to expand in a given tree to reveal specified elements. +- https://github.com/eclipse-sirius/sirius-components/issues/1070[#1070] [tree] The shape of the GraphQL Schema used to send tree instances to the backend has been changed into a flat tree of items (to be rebuilt into a proper tree on the front). This makes the depth of the GraphQL subscription needed to track a particular tree independent on the depth of the tree itself. === Dependency update @@ -52,6 +54,7 @@ - https://github.com/eclipse-sirius/sirius-components/issues/1054[#1054] [diagram] Add missing variables to compute the label of an edge - https://github.com/eclipse-sirius/sirius-components/issues/1063[#1063] [explorer] It is now possible to expand or collapse items in the explorer without selecting them by clicking directly on the expand/collapse arrow icon - https://github.com/eclipse-sirius/sirius-components/issues/1068[#1068] [form] Add support for displaying details on arbitrary element kinds +- https://github.com/eclipse-sirius/sirius-components/issues/1070[#1070] [explorer] When selecting a specific representation (for example from its URL or from the onboard area), it is automatically made visible and selected in the explorer. === New features diff --git a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java index 0f9f722b8db..8d2380f2d92 100644 --- a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java +++ b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeEventProcessorFactory.java @@ -77,6 +77,7 @@ public Optional createRepresentatio TreeCreationParameters treeCreationParameters = TreeCreationParameters.newTreeCreationParameters(treeConfiguration.getId()) .treeDescription(treeDescription) .expanded(treeConfiguration.getExpanded()) + .revealed(treeConfiguration.getRevealed()) .editingContext(editingContext) .build(); // @formatter:on diff --git a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java index 2c8add20e17..92dd040c0fc 100644 --- a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java +++ b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/TreeService.java @@ -35,6 +35,7 @@ public Tree create(TreeCreationParameters treeCreationParameters) { variableManager.put(GetOrCreateRandomIdProvider.PREVIOUS_REPRESENTATION_ID, treeCreationParameters.getId()); variableManager.put(IEditingContext.EDITING_CONTEXT, treeCreationParameters.getEditingContext()); variableManager.put(TreeRenderer.EXPANDED, treeCreationParameters.getExpanded()); + variableManager.put(TreeRenderer.REVEALED, treeCreationParameters.getRevealed()); TreeRenderer treeRenderer = new TreeRenderer(variableManager, treeCreationParameters.getTreeDescription()); return treeRenderer.render(); diff --git a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java index b2cedd79ef8..0a892a25822 100644 --- a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java +++ b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeConfiguration.java @@ -29,10 +29,18 @@ public class TreeConfiguration implements IRepresentationConfiguration { private final List expanded; + private List revealed; + public TreeConfiguration(String editingContextId, List expanded) { - String uniqueId = editingContextId + expanded.toString(); + this(editingContextId, expanded, List.of()); + + } + + public TreeConfiguration(String editingContextId, List expanded, List revealed) { + String uniqueId = editingContextId + expanded.toString() + revealed.toString(); this.treeId = UUID.nameUUIDFromBytes(uniqueId.getBytes()).toString(); this.expanded = Objects.requireNonNull(expanded); + this.revealed = List.copyOf(Objects.requireNonNull(revealed)); } @Override @@ -44,4 +52,7 @@ public List getExpanded() { return this.expanded; } + public List getRevealed() { + return this.revealed; + } } diff --git a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java index bc24143f1a9..04b341ee065 100644 --- a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java +++ b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/TreeCreationParameters.java @@ -33,6 +33,8 @@ public final class TreeCreationParameters { private List expanded; + private List revealed; + private IEditingContext editingContext; private TreeCreationParameters() { @@ -51,6 +53,10 @@ public List getExpanded() { return this.expanded; } + public List getRevealed() { + return this.revealed; + } + public IEditingContext getEditingContext() { return this.editingContext; } @@ -61,8 +67,8 @@ public static Builder newTreeCreationParameters(String id) { @Override public String toString() { - String pattern = "{0} '{'id: {1}, treeDescriptionId: {2}, expanded: {3}'}'"; //$NON-NLS-1$ - return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.treeDescription.getId(), this.expanded); + String pattern = "{0} '{'id: {1}, treeDescriptionId: {2}, expanded: {3}, revealed: {4}'}'"; //$NON-NLS-1$ + return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.treeDescription.getId(), this.expanded, this.revealed); } /** @@ -78,6 +84,8 @@ public static final class Builder { private List expanded; + private List revealed; + private IEditingContext editingContext; private Builder(String id) { @@ -94,6 +102,11 @@ public Builder expanded(List expanded) { return this; } + public Builder revealed(List revealed) { + this.revealed = Objects.requireNonNull(revealed); + return this; + } + public Builder editingContext(IEditingContext editingContext) { this.editingContext = Objects.requireNonNull(editingContext); return this; @@ -104,6 +117,7 @@ public TreeCreationParameters build() { treeCreationParameters.id = Objects.requireNonNull(this.id); treeCreationParameters.treeDescription = Objects.requireNonNull(this.treeDescription); treeCreationParameters.expanded = Objects.requireNonNull(this.expanded); + treeCreationParameters.revealed = Objects.requireNonNull(this.revealed); treeCreationParameters.editingContext = Objects.requireNonNull(this.editingContext); return treeCreationParameters; } diff --git a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java index 3c00f61a927..eb3de5397dd 100644 --- a/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java +++ b/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/TreeEventInput.java @@ -31,14 +31,17 @@ public final class TreeEventInput implements IInput { private List expanded; + private List revealed; + public TreeEventInput() { // Used by Jackson } - public TreeEventInput(UUID id, String editingContextId, List expanded) { + public TreeEventInput(UUID id, String editingContextId, List expanded, List revealed) { this.id = Objects.requireNonNull(id); this.editingContextId = Objects.requireNonNull(editingContextId); - this.expanded = Objects.requireNonNull(expanded); + this.expanded = List.copyOf(Objects.requireNonNull(expanded)); + this.revealed = List.copyOf(Objects.requireNonNull(revealed)); } @Override @@ -54,9 +57,13 @@ public List getExpanded() { return this.expanded; } + public List getRevealed() { + return this.revealed; + } + @Override public String toString() { - String pattern = "{0} '{'id: {1}, editingContextId: {2}, expanded: {3}'}'"; //$NON-NLS-1$ - return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.editingContextId, this.expanded); + String pattern = "{0} '{'id: {1}, editingContextId: {2}, expanded: {3}, revealed: {4}'}'"; //$NON-NLS-1$ + return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.id, this.editingContextId, this.expanded, this.revealed); } } diff --git a/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls b/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls index 69d9bd73983..1640d475244 100644 --- a/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls +++ b/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls @@ -6,6 +6,7 @@ input TreeEventInput { id: ID! editingContextId: ID! expanded: [String!]! + revealed: [String!]! } union TreeEventPayload = ErrorPayload | SubscribersUpdatedEventPayload | TreeRefreshedEventPayload @@ -18,11 +19,13 @@ type TreeRefreshedEventPayload { type Tree implements Representation { id: ID! metadata: RepresentationMetadata! - children: [TreeItem!]! + items: [TreeItem!]! } type TreeItem { id: ID! + parentId: ID + position: Int label: String! kind: String! imageURL: String! @@ -30,7 +33,6 @@ type TreeItem { deletable: Boolean! expanded: Boolean! hasChildren: Boolean! - children: [TreeItem]! } type TreeDescription implements RepresentationDescription { diff --git a/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java b/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java index 1e2a2aba344..df45b34b888 100644 --- a/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java +++ b/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java @@ -55,6 +55,8 @@ public final class TreeDescription implements IRepresentationDescription { private Function> childrenProvider; + private Function> ancestorsProvider; + private Function hasChildrenProvider; private Predicate canCreatePredicate; @@ -115,6 +117,10 @@ public Function> getChildrenProvider() { return this.childrenProvider; } + public Function> getAncestorsProvider() { + return this.ancestorsProvider; + } + public Function getHasChildrenProvider() { return this.hasChildrenProvider; } @@ -171,6 +177,8 @@ public static final class Builder { private Function> childrenProvider; + private Function> ancestorsProvider = variableManager -> List.of(); + private Function hasChildrenProvider; private Predicate canCreatePredicate; @@ -233,6 +241,11 @@ public Builder childrenProvider(Function> childrenProvi return this; } + public Builder ancestorsProvider(Function> ancestorsProvider) { + this.ancestorsProvider = Objects.requireNonNull(ancestorsProvider); + return this; + } + public Builder hasChildrenProvider(Function hasChildrenProvider) { this.hasChildrenProvider = Objects.requireNonNull(hasChildrenProvider); return this; @@ -266,6 +279,7 @@ public TreeDescription build() { treeDescription.deletableProvider = Objects.requireNonNull(this.deletableProvider); treeDescription.elementsProvider = Objects.requireNonNull(this.elementsProvider); treeDescription.childrenProvider = Objects.requireNonNull(this.childrenProvider); + treeDescription.ancestorsProvider = Objects.requireNonNull(this.ancestorsProvider); treeDescription.hasChildrenProvider = Objects.requireNonNull(this.hasChildrenProvider); treeDescription.canCreatePredicate = Objects.requireNonNull(this.canCreatePredicate); treeDescription.deleteHandler = Objects.requireNonNull(this.deleteHandler); diff --git a/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/renderer/TreeRenderer.java b/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/renderer/TreeRenderer.java index 5c08d58d9e3..368379e46eb 100644 --- a/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/renderer/TreeRenderer.java +++ b/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/renderer/TreeRenderer.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.trees.Tree; @@ -30,9 +31,11 @@ public class TreeRenderer { public static final String EXPANDED = "expanded"; //$NON-NLS-1$ - private VariableManager variableManager; + public static final String REVEALED = "revealed"; //$NON-NLS-1$ - private TreeDescription treeDescription; + private final VariableManager variableManager; + + private final TreeDescription treeDescription; public TreeRenderer(VariableManager variableManager, TreeDescription treeDescription) { this.variableManager = Objects.requireNonNull(variableManager); @@ -43,6 +46,12 @@ public Tree render() { String treeId = this.treeDescription.getIdProvider().apply(this.variableManager); String label = this.treeDescription.getLabelProvider().apply(this.variableManager); + List expandedIds = new ArrayList<>(); + expandedIds.addAll(this.getExpandedIds()); + List ancestors = this.treeDescription.getAncestorsProvider().apply(this.variableManager); + expandedIds.addAll(ancestors); + this.variableManager.put(TreeRenderer.EXPANDED, List.copyOf(expandedIds)); + List rootElements = this.treeDescription.getElementsProvider().apply(this.variableManager); List childrenItems = new ArrayList<>(rootElements.size()); for (Object rootElement : rootElements) { @@ -60,6 +69,16 @@ public Tree render() { // @formatter:on } + private List getExpandedIds() { + List expandedIds = new ArrayList<>(); + Object objects = this.variableManager.getVariables().get(TreeRenderer.EXPANDED); + if (objects instanceof List) { + List list = (List) objects; + expandedIds = list.stream().filter(String.class::isInstance).map(String.class::cast).collect(Collectors.toUnmodifiableList()); + } + return expandedIds; + } + private TreeItem renderTreeItem(VariableManager treeItemVariableManager) { String id = this.treeDescription.getTreeItemIdProvider().apply(treeItemVariableManager); String kind = this.treeDescription.getKindProvider().apply(treeItemVariableManager); diff --git a/frontend/src/explorer/ExplorerWebSocketContainer.tsx b/frontend/src/explorer/ExplorerWebSocketContainer.tsx index 7329d257fd3..9230f42f127 100644 --- a/frontend/src/explorer/ExplorerWebSocketContainer.tsx +++ b/frontend/src/explorer/ExplorerWebSocketContainer.tsx @@ -10,7 +10,7 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { gql, useSubscription } from '@apollo/client'; +import { useSubscription } from '@apollo/client'; import { M, Spacing } from 'core/spacing/Spacing'; import { Text } from 'core/text/Text'; import { @@ -20,15 +20,42 @@ import { HANDLE_DATA__ACTION, HANDLE_ERROR__ACTION, HANDLE_EXPANDED__ACTION, + HANDLE_SYNCHRONIZE__ACTION, LOADING__STATE, } from 'explorer/machine'; -import React, { useReducer } from 'react'; +import gql from 'graphql-tag'; +import React, { useEffect, useReducer } from 'react'; import { Explorer } from './Explorer'; import styles from './ExplorerWebSocketContainer.module.css'; import { ExplorerWebSocketContainerProps } from './ExplorerWebSocketContainer.types'; -import { getTreeEventSubscription } from './getTreeEventSubscription'; import { initialState, reducer } from './reducer'; +const treeEventSubscription = gql` + subscription treeEvent($input: TreeEventInput!) { + treeEvent(input: $input) { + __typename + ... on TreeRefreshedEventPayload { + id + tree { + id + items { + id + parentId + position + hasChildren + expanded + label + editable + deletable + kind + imageURL + } + } + } + } + } +`; + export const ExplorerWebSocketContainer = ({ editingContextId, selection, @@ -36,14 +63,20 @@ export const ExplorerWebSocketContainer = ({ readOnly, }: ExplorerWebSocketContainerProps) => { const [state, dispatch] = useReducer(reducer, initialState); - const { viewState, id, tree, expanded, maxDepth, message } = state; + const { viewState, id, tree, expanded, synchronized, message } = state; - const { error } = useSubscription(gql(getTreeEventSubscription(maxDepth)), { + let revealed = []; + if (synchronized && selection.entries.length > 0) { + revealed = selection.entries.map((entry) => entry.kind + '&id=' + entry.id); + } + + const { error } = useSubscription(treeEventSubscription, { variables: { input: { id, editingContextId, expanded, + revealed, }, }, fetchPolicy: 'no-cache', @@ -57,6 +90,11 @@ export const ExplorerWebSocketContainer = ({ dispatch({ type: HANDLE_ERROR__ACTION, message: error }); } + // Enable synchronize mode when the selection is explicitly changed + useEffect(() => { + dispatch({ type: HANDLE_SYNCHRONIZE__ACTION, synchronized: true }); + }, [selection]); + const onExpand = (id: string, depth: number) => { dispatch({ type: HANDLE_EXPANDED__ACTION, id, depth }); }; diff --git a/frontend/src/explorer/__tests__/GraphqlTreeEventSubscription.test.ts b/frontend/src/explorer/__tests__/GraphqlTreeEventSubscription.test.ts deleted file mode 100644 index 5533e26dd7d..00000000000 --- a/frontend/src/explorer/__tests__/GraphqlTreeEventSubscription.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2019, 2021 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 from 'graphql-tag'; -import { getTreeEventSubscription } from '../getTreeEventSubscription'; - -const getDocumentSubscription = gql` - subscription treeEvent($input: TreeEventInput!) { - treeEvent(input: $input) { - __typename - ... on TreeRefreshedEventPayload { - id - tree { - id - children { - ...treeItemFields - children { - ...treeItemFields - children { - ...treeItemFields - } - } - } - } - } - } - } - - fragment treeItemFields on TreeItem { - id - hasChildren - expanded - label - editable - deletable - kind - imageURL - } -`.loc.source.body.trim(); - -describe('TreeEvent - subscription', () => { - it('looks like the graphql subscription loaded from graphql test subscription file', () => { - // apply getTreeEventSubscription with depth 2 - const getBuiltSubscription = getTreeEventSubscription(2); - // compare results - const received = getBuiltSubscription.trim().replace(/\s+/g, ' '); - const expected = getDocumentSubscription.replace(/\s+/g, ' '); - expect(received).toBe(expected); - }); -}); diff --git a/frontend/src/explorer/__tests__/reducer.test.ts b/frontend/src/explorer/__tests__/reducer.test.ts index 91737696ab2..656934ce013 100644 --- a/frontend/src/explorer/__tests__/reducer.test.ts +++ b/frontend/src/explorer/__tests__/reducer.test.ts @@ -21,7 +21,7 @@ import { LOADING__STATE, TREE_LOADED__STATE, } from '../machine'; -import { initialState, reducer } from '../reducer'; +import { buildTree, initialState, reducer } from '../reducer'; const treeLoadedState = { viewState: TREE_LOADED__STATE, @@ -32,7 +32,7 @@ const treeLoadedState = { children: [], }, expanded: [], - maxDepth: 1, + synchronized: true, message: '', }; @@ -45,7 +45,7 @@ const treeLoadedWithErrorState = { children: [], }, expanded: [], - maxDepth: 1, + synchronized: false, message: 'An error has occured while retrieving the content from the server', }; @@ -64,12 +64,18 @@ const treeRefreshEventPayloadMessage = { tree: { id: 'tree', label: 'Project', - children: [], + items: [], }, }, }, }; +const rebuiltTree = { + id: 'tree', + label: 'Project', + children: [], +}; + describe('ExplorerWebSocketContainer - reducer', () => { it('has a proper initial state', () => { expect(initialState).toStrictEqual({ @@ -77,7 +83,7 @@ describe('ExplorerWebSocketContainer - reducer', () => { id: initialState.id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: true, message: '', modal: undefined, }); @@ -96,7 +102,7 @@ describe('ExplorerWebSocketContainer - reducer', () => { id: prevState.id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: false, message: 'An error has occured while retrieving the content from the server', modal: undefined, }); @@ -113,7 +119,7 @@ describe('ExplorerWebSocketContainer - reducer', () => { id: prevState.id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: false, message: message, modal: undefined, }); @@ -128,9 +134,9 @@ describe('ExplorerWebSocketContainer - reducer', () => { expect(state).toStrictEqual({ viewState: TREE_LOADED__STATE, id: prevState.id, - tree: message.data.treeEvent.tree, + tree: rebuiltTree, expanded: [], - maxDepth: 1, + synchronized: true, message: '', modal: undefined, }); @@ -145,9 +151,9 @@ describe('ExplorerWebSocketContainer - reducer', () => { expect(state).toStrictEqual({ viewState: TREE_LOADED__STATE, id: prevState.id, - tree: message.data.treeEvent.tree, + tree: rebuiltTree, expanded: [], - maxDepth: 1, + synchronized: prevState.synchronized, message: '', modal: undefined, }); @@ -164,7 +170,7 @@ describe('ExplorerWebSocketContainer - reducer', () => { id: prevState.id, tree: prevState.tree, expanded: prevState.expanded, - maxDepth: prevState.maxDepth, + synchronized: prevState.synchronized, message: message, modal: undefined, }); @@ -179,9 +185,9 @@ describe('ExplorerWebSocketContainer - reducer', () => { expect(state).toStrictEqual({ viewState: TREE_LOADED__STATE, id: prevState.id, - tree: message.data.treeEvent.tree, + tree: rebuiltTree, expanded: [], - maxDepth: 1, + synchronized: prevState.synchronized, message: '', modal: undefined, }); @@ -198,9 +204,99 @@ describe('ExplorerWebSocketContainer - reducer', () => { id: prevState.id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: false, message: '', modal: undefined, }); }); }); + +describe('ExplorerWebSocketContainer - buildTree', () => { + it('can rebuild an empty tree', () => { + const flatTree = { + id: 42, + label: 'Empty Tree', + items: [], + }; + + const { tree, expanded } = buildTree(flatTree); + expect(tree).toStrictEqual({ + id: 42, + label: 'Empty Tree', + children: [], + }); + expect(expanded).toStrictEqual([]); + }); + + it('can rebuild 1-depth tree', () => { + const flatTree = { + id: 42, + label: 'Tree with root items', + items: [ + // The order in the flat list should not be relevant + { id: 'child4', parentId: 42, position: 3, label: 'Child 4', expanded: false }, + { id: 'child1', parentId: 42, position: 0, label: 'Child 1', expanded: true }, + { id: 'child6', parentId: 42, position: 5, label: 'Child 6', expanded: false }, + { id: 'child2', parentId: 42, position: 1, label: 'Child 2', expanded: false }, + { id: 'child3', parentId: 42, position: 2, label: 'Child 3', expanded: false }, + { id: 'child5', parentId: 42, position: 4, label: 'Child 5', expanded: true }, + ], + }; + + const { tree, expanded } = buildTree(flatTree); + expect(tree).toStrictEqual({ + id: 42, + label: 'Tree with root items', + children: [ + { id: 'child1', label: 'Child 1', expanded: true, children: [] }, + { id: 'child2', label: 'Child 2', expanded: false, children: [] }, + { id: 'child3', label: 'Child 3', expanded: false, children: [] }, + { id: 'child4', label: 'Child 4', expanded: false, children: [] }, + { id: 'child5', label: 'Child 5', expanded: true, children: [] }, + { id: 'child6', label: 'Child 6', expanded: false, children: [] }, + ], + }); + expect(expanded.sort()).toStrictEqual(['child1', 'child5']); + }); + + it('can rebuild 3-depth tree', () => { + const flatTree = { + id: 42, + label: 'Tree 3 levels of items', + items: [ + // The order in the flat list should not be relevant + { id: 'item1.1', parentId: 'item1', position: 0, label: 'Item 1.1', expanded: true }, + { id: 'item1.1.1', parentId: 'item1.1', position: 0, label: 'Item 1.1.1', expanded: true }, + { id: 'item1.2', parentId: 'item1', position: 1, label: 'Item 1.2', expanded: false }, + { id: 'item1', parentId: 42, position: 0, label: 'Item 1', expanded: true }, + { id: 'item1.1.2', parentId: 'item1.1', position: 1, label: 'Item 1.1.2', expanded: false }, + ], + }; + + const { tree, expanded } = buildTree(flatTree); + expect(tree).toStrictEqual({ + id: 42, + label: 'Tree 3 levels of items', + children: [ + { + id: 'item1', + label: 'Item 1', + expanded: true, + children: [ + { + id: 'item1.1', + label: 'Item 1.1', + expanded: true, + children: [ + { id: 'item1.1.1', label: 'Item 1.1.1', expanded: true, children: [] }, + { id: 'item1.1.2', label: 'Item 1.1.2', expanded: false, children: [] }, + ], + }, + { id: 'item1.2', label: 'Item 1.2', expanded: false, children: [] }, + ], + }, + ], + }); + expect(expanded.sort()).toStrictEqual(['item1', 'item1.1', 'item1.1.1']); + }); +}); diff --git a/frontend/src/explorer/getTreeEventSubscription.ts b/frontend/src/explorer/getTreeEventSubscription.ts deleted file mode 100644 index f4c6b9777f6..00000000000 --- a/frontend/src/explorer/getTreeEventSubscription.ts +++ /dev/null @@ -1,56 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2019, 2021 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 const getTreeEventSubscription = (depth) => { - const treeChildren = recursiveGetChildren(depth); - const subscription = ` -subscription treeEvent($input: TreeEventInput!) { - treeEvent(input: $input) { - __typename - ... on TreeRefreshedEventPayload { - id - tree { - id - children { - ${treeChildren} - } - } - } - } -} -`; - return subscription + fragment; -}; - -const fragment = ` -fragment treeItemFields on TreeItem { - id - hasChildren - expanded - label - editable - deletable - kind - imageURL -} -`; - -const recursiveGetChildren = (depth) => { - let children = ''; - if (depth > 0) { - children = ` - children { - ${recursiveGetChildren(depth - 1)} - }`; - } - return `...treeItemFields${children}`; -}; diff --git a/frontend/src/explorer/machine.ts b/frontend/src/explorer/machine.ts index 84187a88616..67b7f42dcc4 100644 --- a/frontend/src/explorer/machine.ts +++ b/frontend/src/explorer/machine.ts @@ -20,6 +20,7 @@ export const HANDLE_CONNECTION_ERROR__ACTION = 'HANDLE_CONNECTION_ERROR__ACTION' export const HANDLE_ERROR__ACTION = 'HANDLE_ERROR__ACTION'; export const HANDLE_COMPLETE__ACTION = 'HANDLE_COMPLETE__ACTION'; export const HANDLE_EXPANDED__ACTION = 'HANDLE_EXPANDED__ACTION'; +export const HANDLE_SYNCHRONIZE__ACTION = 'HANDLE_SYNCHRONIZE__ACTION'; export const machine = { LOADING__STATE: { @@ -27,12 +28,14 @@ export const machine = { HANDLE_ERROR__ACTION: [ERROR__STATE], HANDLE_DATA__ACTION: [ERROR__STATE, TREE_LOADED__STATE], HANDLE_COMPLETE__ACTION: [COMPLETE__STATE], + HANDLE_SYNCHRONIZE__ACTION: [LOADING__STATE], }, TREE_LOADED__STATE: { HANDLE_ERROR__ACTION: [TREE_LOADED__STATE], HANDLE_DATA__ACTION: [TREE_LOADED__STATE], HANDLE_COMPLETE__ACTION: [COMPLETE__STATE], HANDLE_EXPANDED__ACTION: [TREE_LOADED__STATE], + HANDLE_SYNCHRONIZE__ACTION: [TREE_LOADED__STATE], }, ERROR__STATE: { HANDLE_ERROR__ACTION: [ERROR__STATE], diff --git a/frontend/src/explorer/reducer.ts b/frontend/src/explorer/reducer.ts index 5060ddc694a..5c82899bff5 100644 --- a/frontend/src/explorer/reducer.ts +++ b/frontend/src/explorer/reducer.ts @@ -19,6 +19,7 @@ import { HANDLE_DATA__ACTION, HANDLE_ERROR__ACTION, HANDLE_EXPANDED__ACTION, + HANDLE_SYNCHRONIZE__ACTION, LOADING__STATE, machine, TREE_LOADED__STATE, @@ -29,7 +30,7 @@ export const initialState = { id: uuid(), tree: undefined, expanded: [], - maxDepth: 1, + synchronized: true, message: '', modal: undefined, }; @@ -57,6 +58,9 @@ export const reducer = (prevState, action) => { case HANDLE_EXPANDED__ACTION: state = handleExpandedAction(prevState, action); break; + case HANDLE_SYNCHRONIZE__ACTION: + state = handleSynchronizeAction(prevState, action); + break; default: state = prevState; break; @@ -76,21 +80,70 @@ const handleConnectionErrorAction = (prevState) => { id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: false, message: 'An error has occured while retrieving the content from the server', modal: undefined, }; }; +/** + * Rebuild an actual tree from a flat list of items. + * @param flatTree the flat list of items, each with the id of its parent and its position in the parent. + * @returns an equivalent tree. + */ +export const buildTree = (flatTree) => { + const root = { id: flatTree.id, label: flatTree.label, children: [] }; + const expanded = []; + const allItems = []; + const byParentId = {}; + + // Build the actual TreeItems and group them by parentId + for (let i = 0; i < flatTree.items.length; i++) { + const flatItem = flatTree.items[i]; + const treeItem = { ...flatItem }; + //treeItem['__typename'] = 'TreeItem'; + treeItem.children = []; + + if (!byParentId[flatItem.parentId]) { + byParentId[flatItem.parentId] = []; + } + allItems.push(treeItem); + byParentId[flatItem.parentId].push(treeItem); + } + + // Move the items into their parent's children list + if (byParentId[root.id]) { + byParentId[root.id].sort((a, b) => a.position - b.position); + byParentId[root.id].forEach((child) => root.children.push(child)); + } + allItems.forEach((item) => { + if (item.expanded) { + expanded.push(item.id); + } + if (byParentId[item.id]) { + byParentId[item.id].sort((a, b) => a.position - b.position); + byParentId[item.id].forEach((child) => { + item.children.push(child); + }); + } + // Remove the fields which were only needed in the flat representation + delete item['parentId']; + delete item['position']; + }); + + return { tree: root, expanded }; +}; + const handleDataAction = (prevState, action) => { - const { id, expanded, maxDepth, modal } = prevState; + const { id, synchronized, modal } = prevState; const { message } = action; if (message?.data?.treeEvent) { const { treeEvent } = message.data; if (treeEvent.__typename === 'TreeRefreshedEventPayload') { - const { tree } = treeEvent; - return { viewState: TREE_LOADED__STATE, id, tree, expanded, maxDepth, message: '', modal }; + const { tree: flatTree } = treeEvent; + const { tree, expanded } = buildTree(flatTree); + return { viewState: TREE_LOADED__STATE, id, tree, expanded, synchronized, message: '', modal }; } } @@ -98,12 +151,12 @@ const handleDataAction = (prevState, action) => { }; const handleErrorAction = (prevState, action) => { - const { viewState, id, tree, expanded, maxDepth, modal } = prevState; + const { viewState, id, tree, expanded, synchronized, modal } = prevState; const { message } = action; if (viewState === TREE_LOADED__STATE) { - return { viewState: TREE_LOADED__STATE, id, tree, expanded, maxDepth, message, modal }; + return { viewState: TREE_LOADED__STATE, id, tree, expanded, synchronized, message, modal }; } - return { viewState: ERROR__STATE, id, tree, expanded, maxDepth, message, modal }; + return { viewState: ERROR__STATE, id, tree, expanded, synchronized: false, message, modal }; }; const handleCompleteAction = (prevState) => { @@ -113,22 +166,38 @@ const handleCompleteAction = (prevState) => { id, tree: undefined, expanded: [], - maxDepth: 1, + synchronized: false, message: '', modal: undefined, }; }; const handleExpandedAction = (prevState, action) => { - const { viewState, id, tree, expanded, maxDepth, message, modal } = prevState; - const { id: elementId, depth } = action; + const { viewState, id, tree, expanded, synchronized, message, modal } = prevState; + const { id: elementId } = action; + let newSynchronized = synchronized; let newExpanded; if (expanded.includes(elementId)) { newExpanded = [...expanded]; newExpanded.splice(newExpanded.indexOf(elementId), 1); + newSynchronized = false; // Disable synchronize mode on collapse } else { newExpanded = [...expanded, elementId]; } - return { viewState, id, tree, expanded: newExpanded, maxDepth: Math.max(maxDepth, depth), message, modal }; + return { + viewState, + id, + tree, + expanded: newExpanded, + synchronized: newSynchronized, + message, + modal, + }; +}; + +const handleSynchronizeAction = (prevState, action) => { + const { viewState, id, tree, expanded, message, modal } = prevState; + const { synchronized } = action; + return { viewState, id, tree, expanded, synchronized, message, modal }; }; diff --git a/frontend/src/tree/TreeItem.tsx b/frontend/src/tree/TreeItem.tsx index 3fa0742f474..755f0649caa 100644 --- a/frontend/src/tree/TreeItem.tsx +++ b/frontend/src/tree/TreeItem.tsx @@ -204,7 +204,7 @@ export const TreeItem = ({ } let children = null; - if (item.expanded) { + if (item.expanded && item.children) { children = (
    {item.children.map((childItem) => { diff --git a/frontend/src/workbench/Workbench.tsx b/frontend/src/workbench/Workbench.tsx index 60bc31b5640..386462e8e3e 100644 --- a/frontend/src/workbench/Workbench.tsx +++ b/frontend/src/workbench/Workbench.tsx @@ -85,6 +85,7 @@ export const Workbench = ({ const { registry } = useContext(RepresentationContext); const [{ value, context }, dispatch] = useMachine(workbenchMachine, { context: { + selection: { entries: initialRepresentationSelected ? [initialRepresentationSelected] : [] }, displayedRepresentation: initialRepresentationSelected, representations: initialRepresentationSelected ? [initialRepresentationSelected] : [], },