Skip to content

Commit

Permalink
[1141] Add support for a new 'Related Elements' view
Browse files Browse the repository at this point in the history
Bug: #1141
Signed-off-by: Pierre-Charles David <[email protected]>
  • Loading branch information
pcdavid authored and sbegaudeau committed Jul 21, 2022
1 parent a871ddc commit f5219c6
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 66 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
- [ADR-63] Simplify the Sprotty integration
- [ADR-64] Add support for a ToggableAreaContainer in forms
- [ADR-65] Add support for a tree widget in forms
- [ADR-66] Add a new "Related Elements" view

=== Deprecation warnings


=== Breaking changes

- https://github.com/eclipse-sirius/sirius-components/issues/1141[#1141] [frontend] The `PropertiesWebSocketContainer` which was hard-coded to display the `propertiesEvent` subscription has been made more generic and renamed `FormBasedView`.
It now takes an additional prop named `subscriptionName` so that it can be bound to other subscriptions (which must follow the same API/protocol and send Form payloads).
To integrate the "Details" view inside a workbench, one must now use the new `DetailsView` component (which simply configures `FormBasedView` to listen to `propertiesEvent` as before).

=== Dependency update

- The frontend now depends on `@material-ui/lab` to support the new https://github.com/eclipse-sirius/sirius-components/issues/1139[tree widget] (see ADR-065).
Expand All @@ -33,6 +38,9 @@
- https://github.com/eclipse-sirius/sirius-components/issues/1242[#1242] [view] Provide icons for View DSL Widgets and FormDescription
- https://github.com/eclipse-sirius/sirius-components/issues/1214[#1214] [forms] The mandatory label attribute which was available in all concrete widget types is now explicitly part of the common supertype.
- https://github.com/eclipse-sirius/sirius-components/issues/1215[#1215] [forms] Widgets can now optionally specify an `iconURL`. It is not used in the default UI for the details view or forms representations for now, but icons (if present) are visible in the toggle buttons bar for groups using the new `displayMode = TOGGLEABLE_AREAS`.
- https://github.com/eclipse-sirius/sirius-components/issues/1141[#1141] [workbench] It is now possible to add a "Related View" contribution to the workbench.
It behaves like the "Details" view in that it reacts to the selection by displaying a Form, but is designed to display contextual information about the selected element (i.e. which elements it points to and which elements point to it for example).
The actual definition of the application (e.g. Sirius Web), which must provide a single bean implementing the new `IRelatedElementsDescriptionProvider` interface.

=== New features

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.eclipse.sirius.components.collaborative.api.IRepresentationConfiguration;
import org.eclipse.sirius.components.collaborative.api.IRepresentationEventProcessor;
import org.eclipse.sirius.components.collaborative.api.IRepresentationEventProcessorFactory;
import org.eclipse.sirius.components.collaborative.api.IRepresentationRefreshPolicyRegistry;
import org.eclipse.sirius.components.collaborative.api.ISubscriptionManagerFactory;
import org.eclipse.sirius.components.collaborative.forms.api.FormCreationParameters;
import org.eclipse.sirius.components.collaborative.forms.api.IFormEventHandler;
import org.eclipse.sirius.components.collaborative.forms.api.IFormEventProcessor;
import org.eclipse.sirius.components.collaborative.forms.api.IRelatedElementsDescriptionProvider;
import org.eclipse.sirius.components.collaborative.forms.api.IWidgetSubscriptionManagerFactory;
import org.eclipse.sirius.components.collaborative.forms.api.RelatedElementsConfiguration;
import org.eclipse.sirius.components.core.api.IEditingContext;
import org.eclipse.sirius.components.core.api.IObjectService;
import org.eclipse.sirius.components.forms.description.FormDescription;
import org.springframework.stereotype.Service;

/**
* Used to create the related elements event processors.
*
* @author pcdavid
*/
@Service
public class RelatedElementsEventProcessorFactory implements IRepresentationEventProcessorFactory {

private final IRelatedElementsDescriptionProvider relatedElementsDescriptionProvider;

private final IObjectService objectService;

private final List<IFormEventHandler> formEventHandlers;

private final ISubscriptionManagerFactory subscriptionManagerFactory;

private final IWidgetSubscriptionManagerFactory widgetSubscriptionManagerFactory;

private final IRepresentationRefreshPolicyRegistry representationRefreshPolicyRegistry;

public RelatedElementsEventProcessorFactory(IRelatedElementsDescriptionProvider relatedElementsDescriptionProvider, IObjectService objectService, List<IFormEventHandler> formEventHandlers,
ISubscriptionManagerFactory subscriptionManagerFactory, IWidgetSubscriptionManagerFactory widgetSubscriptionManagerFactory,
IRepresentationRefreshPolicyRegistry representationRefreshPolicyRegistry) {
this.relatedElementsDescriptionProvider = Objects.requireNonNull(relatedElementsDescriptionProvider);
this.objectService = Objects.requireNonNull(objectService);
this.formEventHandlers = Objects.requireNonNull(formEventHandlers);
this.subscriptionManagerFactory = Objects.requireNonNull(subscriptionManagerFactory);
this.widgetSubscriptionManagerFactory = Objects.requireNonNull(widgetSubscriptionManagerFactory);
this.representationRefreshPolicyRegistry = Objects.requireNonNull(representationRefreshPolicyRegistry);
}

@Override
public <T extends IRepresentationEventProcessor> boolean canHandle(Class<T> representationEventProcessorClass, IRepresentationConfiguration configuration) {
return IFormEventProcessor.class.isAssignableFrom(representationEventProcessorClass) && configuration instanceof RelatedElementsConfiguration;
}

@Override
public <T extends IRepresentationEventProcessor> Optional<T> createRepresentationEventProcessor(Class<T> representationEventProcessorClass, IRepresentationConfiguration configuration,
IEditingContext editingContext) {
if (IFormEventProcessor.class.isAssignableFrom(representationEventProcessorClass) && configuration instanceof RelatedElementsConfiguration) {
RelatedElementsConfiguration relatedElementsConfiguration = (RelatedElementsConfiguration) configuration;

// @formatter:off
var objects = relatedElementsConfiguration.getObjectIds().stream()
.map(objectId -> this.objectService.getObject(editingContext, objectId))
.flatMap(Optional::stream)
.collect(Collectors.toList());
// @formatter:on
if (!objects.isEmpty()) {
FormDescription formDescription = this.relatedElementsDescriptionProvider.getFormDescription();
// @formatter:off
FormCreationParameters formCreationParameters = FormCreationParameters.newFormCreationParameters(relatedElementsConfiguration.getId())
.editingContext(editingContext)
.formDescription(formDescription)
.objects(objects)
.build();
// @formatter:on

IRepresentationEventProcessor formEventProcessor = new FormEventProcessor(formCreationParameters, this.formEventHandlers, this.subscriptionManagerFactory.create(),
this.widgetSubscriptionManagerFactory.create(), this.representationRefreshPolicyRegistry);

// @formatter:off
return Optional.of(formEventProcessor)
.filter(representationEventProcessorClass::isInstance)
.map(representationEventProcessorClass::cast);
// @formatter:on
}
}
return Optional.empty();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.api;

import org.eclipse.sirius.components.forms.description.FormDescription;

/**
* Interface used to contribute the form to display for the "Related Elements" view.
*
* @author pcdavid
*/
public interface IRelatedElementsDescriptionProvider {
FormDescription getFormDescription();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.components.collaborative.forms.api;

import java.util.List;
import java.util.Objects;
import java.util.UUID;

import org.eclipse.sirius.components.collaborative.api.IRepresentationConfiguration;

/**
* The configuration used to create a related elements event processor.
*
* @author pcdavid
*/
public class RelatedElementsConfiguration implements IRepresentationConfiguration {

private static final String RELATED_PREFIX = "related:"; //$NON-NLS-1$

private final String formId;

private final List<String> objectIds;

public RelatedElementsConfiguration(List<String> objectIds) {
this.objectIds = Objects.requireNonNull(objectIds);
this.formId = UUID.nameUUIDFromBytes((RELATED_PREFIX + objectIds).getBytes()).toString();
}

@Override
public String getId() {
return this.formId;
}

public List<String> getObjectIds() {
return this.objectIds;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extend type Subscription {
formEvent(input: FormEventInput!): FormEventPayload!
propertiesEvent(input: PropertiesEventInput!): PropertiesEventPayload!
relatedElementsEvent(input: PropertiesEventInput!): PropertiesEventPayload!
representationsEvent(input: RepresentationsEventInput!): RepresentationsEventPayload!
}

Expand Down
103 changes: 103 additions & 0 deletions doc/adrs/066_add_related_elements_view.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
= ADR-066 - Add a new "Related Elements" view

== Context

Capella provides a very useful __Semantic Browser_ view which gives end-users contextual informations about the currently selected element.
When an element is selected this view displays 3 tree widgets:

- _Referencing_: shows elements of interest that refer to the current selecion.
- _Current_: shows elements directly associated to the current selection.
- _Referenced_: shows elements of interest that the current selecion refers to.

We want a similar view in Sirius Components that applications (like Sirius Web) can decide to intergrate (or not) in their workbench.
It should be extensible so that the actual information displayed can be customized/extended for specific applications or domains.
It should provide a default generic implementation that provides useful information for an arbirtary semantic element.

== Decision

We will provide a new _Related Elements_ view component that can be integrated into a workbench using a `WorkbenchViewContribution`:

```
<WorkbenchViewContribution
side="right"
title="Related Elements"
icon={<LinkIcon />}
component={RelatedElementsView}
/>
```

This new view will behave almost exactly like the _Details_ view, in that it will display an arbitrary _Form_, synchronized with the (possibly multiple) selection.
The only difference with the _Details_ view is that it will connect to a different (new) GraphQL subscription:

```
// In form.graphqls
extend type Subscription {
propertiesEvent(input: PropertiesEventInput!): PropertiesEventPayload! // existing
relatedElementsEvent(input: PropertiesEventInput!): PropertiesEventPayload! // added
}
```

=== Generic PropertiesWebSocketContainer

To avoid duplicating too much frontend code, the existing `PropertiesWebSocketContainer` will be made more generic and take the name of the GraphQL subscription to use as an argument.

The two concrete views (_Details_ and _Related Elements_) will use this new generic component like this:

```
// Replaces the old, hard-coded PropertiesWebSocketContainer
export const DetailsView = (props: WorkbenchViewComponentProps) => (
<PropertiesWebSocketContainer {...props} subscriptionName={'propertiesEvent'} />
);

export const RelatedElementsView = (props: WorkbenchViewComponentProps) => (
<PropertiesWebSocketContainer {...props} subscriptionName={'relatedElementsEvent'} />
);
```

Applications which used to include the `PropertiesWebSocketContainer` component will need to be updated to use the new `DetailsView` component instead.

NOTE: The `PropertiesWebSocketContainer` could be renamed with a more generic name, like `FormWebSocketContainer`, or more simply `FormBasedView` to reflect its more generic nature, but this is defered for later to reduce the impacts of this new feature.

This change requires modifications in the `GQL*` types used by `PropertiesWebSocketContainer` as they currently hard-code in their structure the fact that the form data is inside a `propertiesEvent` subscription.
Because the now generic/configurable name can not be encoded in the static type structure, the corresponding state machine must be modified so that `HandleSubscriptionResultEvent` takes the `GQLPropertiesEventPayload` directly as a `result`, instead of a `SubscriptionResult<GQLPropertiesEventSubscription>` where `GQLPropertiesEventSubscription` hard-code the `propertiesEvent` name.

The GraphQL subscription used to be static (known at compile-time), and use the `gql` template literal tag from `'graphql-tag'` to parse it.
Because the text of the subscription is now parameterized by a string, we must use the `gql` _function_ instead (as already done in `ExplorerWebSocketContainer` with `getTreeEventSubscription`).

The `RepresentationsWebSocketContainer` is also impacted by this change, as it uses the GraphQL fragments defined in `FormEventFragments.ts`.

=== IRelatedElementsDescriptionProvider

On the backend, a new `RelatedElementsEventProcessorFactory` (which implements `IRepresentationEventProcessorFactory`) will be added.
To keep things simple in the first version of this feature, we will assume there is a single, system-wide `IRelatedElementsDescriptionProvider` bean which provides the `FormDescription` to use for this view:

```
public interface IRelatedElementsDescriptionProvider {
FormDescription getFormDescription();
}
```

Future versions may provide some ways to customize and/or extend the content of the new view for different semantic elements.

Applications that want to use the new view will need to provide a single bean implementing this interface.

=== Default implementation

Sirius Web will provide a default implementation named `DefaultRelatedElementsDescriptionProvider`.

It will display 3 _Tree_ widgets inside a single Group configured using the new `displayMode = TOGGLEABLE_AREAS`.

* A tree named "Incoming", which displays all the elements that reference the current element.
These referecing elements are organized into folders/categories that correspond to the (EMF) reference through which they reference the element.
It is possible for a referencing element to appear in multiple categories if it references the current element multiple times.
* A tree named "Current" with the following folders/categories:

- "Parent", with a single node for the parent of the current element
- One folder per containment reference (named after the reference) with all the direct children of the current element owned through that reference

* A tree name "Outgoing", which displays all the elements referenced by the current element (exluding its children).
As for the "Incoming" tree, the elements are organized into categories that correspond to the EMF reference.

== Status

Accepted.
19 changes: 19 additions & 0 deletions frontend/src/details/DetailsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*******************************************************************************
* Copyright (c) 2022 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 { FormBasedView } from 'properties/FormBasedView';
import React from 'react';
import { WorkbenchViewComponentProps } from 'workbench/Workbench.types';

export const DetailsView = (props: WorkbenchViewComponentProps) => (
<FormBasedView {...props} subscriptionName="propertiesEvent" />
);
7 changes: 3 additions & 4 deletions frontend/src/form/FormEventFragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import gql from 'graphql-tag';

export const subscribersUpdatedEventPayloadFragment = gql`
export const subscribersUpdatedEventPayloadFragment = `
fragment subscribersUpdatedEventPayloadFragment on SubscribersUpdatedEventPayload {
id
subscribers {
Expand All @@ -21,7 +20,7 @@ export const subscribersUpdatedEventPayloadFragment = gql`
}
`;

export const widgetSubscriptionsUpdatedEventPayloadFragment = gql`
export const widgetSubscriptionsUpdatedEventPayloadFragment = `
fragment widgetSubscriptionsUpdatedEventPayloadFragment on WidgetSubscriptionsUpdatedEventPayload {
id
widgetSubscriptions {
Expand All @@ -33,7 +32,7 @@ export const widgetSubscriptionsUpdatedEventPayloadFragment = gql`
}
`;

export const formRefreshedEventPayloadFragment = gql`
export const formRefreshedEventPayloadFragment = `
fragment formRefreshedEventPayloadFragment on FormRefreshedEventPayload {
id
form {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/form/FormEventFragments.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface GQLPropertiesEventSubscription {
propertiesEvent: GQLPropertiesEventPayload;
}

export interface GQLRelatedElementsEventSubscription {
relatedElementsEvent: GQLPropertiesEventPayload;
}

export interface GQLPropertiesEventPayload {
__typename: string;
}
Expand Down
Loading

0 comments on commit f5219c6

Please sign in to comment.