diff --git a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItem.java b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItem.java index 5679f06767..6bf8b037bf 100644 --- a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItem.java +++ b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItem.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2020 Obeo. + * 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 @@ -35,6 +35,8 @@ public final class ListItem { private String imageURL; + private ListItemAction action; + private ListItem() { // Prevent instantiation } @@ -58,6 +60,12 @@ public String getImageURL() { return this.imageURL; } + @GraphQLField + @GraphQLNonNull + public ListItemAction getAction() { + return this.action; + } + public static Builder newListItem(String id) { return new Builder(id); } @@ -81,6 +89,8 @@ public static final class Builder { private String imageURL; + private ListItemAction action; + private Builder(String id) { this.id = Objects.requireNonNull(id); } @@ -95,11 +105,17 @@ public Builder imageURL(String imageURL) { return this; } + public Builder action(ListItemAction action) { + this.action = Objects.requireNonNull(action); + return this; + } + public ListItem build() { ListItem listItem = new ListItem(); listItem.id = Objects.requireNonNull(this.id); listItem.label = Objects.requireNonNull(this.label); listItem.imageURL = Objects.requireNonNull(this.imageURL); + listItem.action = Objects.requireNonNull(this.action); return listItem; } } diff --git a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItemAction.java b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItemAction.java new file mode 100644 index 0000000000..4dfddb747e --- /dev/null +++ b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/ListItemAction.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 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 + *******************************************************************************/ +package org.eclipse.sirius.web.forms; + +import java.text.MessageFormat; +import java.util.Objects; + +import org.eclipse.sirius.web.annotations.Immutable; +import org.eclipse.sirius.web.annotations.graphql.GraphQLField; +import org.eclipse.sirius.web.annotations.graphql.GraphQLNonNull; +import org.eclipse.sirius.web.annotations.graphql.GraphQLObjectType; + +/** + * The action of a {@link ListItem}. + * + * @author gcoutable + */ +@Immutable +@GraphQLObjectType +public final class ListItemAction { + + private String tooltip; + + private String iconName; + + private ListItemAction() { + // Prevent instantiation + } + + @GraphQLField + @GraphQLNonNull + public String getTooltip() { + return this.tooltip; + } + + @GraphQLField + @GraphQLNonNull + public String getIconName() { + return this.iconName; + } + + public static Builder newListItemAction() { + return new Builder(); + } + + @Override + public String toString() { + String pattern = "{0} '{'tooltip: {1}, iconName: {2}'}'"; //$NON-NLS-1$ + return MessageFormat.format(pattern, this.getClass().getSimpleName(), this.tooltip, this.iconName); + } + + /** + * The builder used to create the list item action. + * + * @author gcoutable + */ + @SuppressWarnings("checkstyle:HiddenField") + public static final class Builder { + private String tooltip; + + private String iconName; + + public Builder tooltip(String tooltip) { + this.tooltip = Objects.requireNonNull(tooltip); + return this; + } + + public Builder iconName(String iconName) { + this.iconName = Objects.requireNonNull(iconName); + return this; + } + + public ListItemAction build() { + ListItemAction listItemAction = new ListItemAction(); + listItemAction.tooltip = Objects.requireNonNull(this.tooltip); + listItemAction.iconName = Objects.requireNonNull(this.iconName); + return listItemAction; + } + } + +} diff --git a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/components/ListComponent.java b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/components/ListComponent.java index 17339209ad..42d94d0022 100644 --- a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/components/ListComponent.java +++ b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/components/ListComponent.java @@ -19,6 +19,7 @@ import org.eclipse.sirius.web.components.Element; import org.eclipse.sirius.web.components.IComponent; import org.eclipse.sirius.web.forms.ListItem; +import org.eclipse.sirius.web.forms.ListItemAction; import org.eclipse.sirius.web.forms.description.ListDescription; import org.eclipse.sirius.web.forms.elements.ListElementProps; import org.eclipse.sirius.web.forms.validation.DiagnosticComponent; @@ -32,6 +33,8 @@ */ public class ListComponent implements IComponent { + public static final String ACTION_VARIABLE = "action"; //$NON-NLS-1$ + public static final String CANDIDATE_VARIABLE = "candidate"; //$NON-NLS-1$ private ListComponentProps props; @@ -60,10 +63,22 @@ public Element render() { String itemLabel = listDescription.getItemLabelProvider().apply(itemVariableManager); String itemImageURL = listDescription.getItemImageURLProvider().apply(itemVariableManager); + Object listItemAction = listDescription.getListItemActionProvider().apply(itemVariableManager); + VariableManager itemActionVariableManager = itemVariableManager.createChild(); + itemActionVariableManager.put(ACTION_VARIABLE, listItemAction); + String listItemActionTooltip = listDescription.getListItemActionTooltipProvider().apply(itemActionVariableManager); + String listItemActionIconName = listDescription.getListItemActionIconNameProvider().apply(itemActionVariableManager); + // @formatter:off + ListItemAction itemAction = ListItemAction.newListItemAction() + .tooltip(listItemActionTooltip) + .iconName(listItemActionIconName) + .build(); + ListItem item = ListItem.newListItem(itemId) .label(itemLabel) .imageURL(itemImageURL) + .action(itemAction) .build(); // @formatter:on diff --git a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/description/ListDescription.java b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/description/ListDescription.java index cc88b49400..2ec2582e4e 100644 --- a/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/description/ListDescription.java +++ b/backend/sirius-web-forms/src/main/java/org/eclipse/sirius/web/forms/description/ListDescription.java @@ -40,6 +40,12 @@ public final class ListDescription extends AbstractWidgetDescription { private Function itemImageURLProvider; + private Function listItemActionProvider; + + private Function listItemActionTooltipProvider; + + private Function listItemActionIconNameProvider; + private ListDescription() { // Prevent instantiation } @@ -68,6 +74,18 @@ public Function getItemImageURLProvider() { return this.itemImageURLProvider; } + public Function getListItemActionProvider() { + return this.listItemActionProvider; + } + + public Function getListItemActionTooltipProvider() { + return this.listItemActionTooltipProvider; + } + + public Function getListItemActionIconNameProvider() { + return this.listItemActionIconNameProvider; + } + public static Builder newListDescription(String id) { return new Builder(id); } @@ -99,6 +117,12 @@ public static final class Builder { private Function itemImageURLProvider; + private Function listItemActionProvider; + + private Function listItemActionTooltipProvider; + + private Function listItemActionIconNameProvider; + private Function> diagnosticsProvider; private Function kindProvider; @@ -139,6 +163,21 @@ public Builder itemImageURLProvider(Function itemImageU return this; } + public Builder listItemActionProvider(Function listItemActionProvider) { + this.listItemActionProvider = Objects.requireNonNull(listItemActionProvider); + return this; + } + + public Builder listItemActionTooltipProvider(Function listItemActionTooltipProvider) { + this.listItemActionTooltipProvider = Objects.requireNonNull(listItemActionTooltipProvider); + return this; + } + + public Builder listItemActionIconNameProvider(Function listItemActionIconNameProvider) { + this.listItemActionIconNameProvider = Objects.requireNonNull(listItemActionIconNameProvider); + return this; + } + public Builder diagnosticsProvider(Function> diagnosticsProvider) { this.diagnosticsProvider = Objects.requireNonNull(diagnosticsProvider); return this; @@ -163,6 +202,9 @@ public ListDescription build() { listDescription.itemIdProvider = Objects.requireNonNull(this.itemIdProvider); listDescription.itemLabelProvider = Objects.requireNonNull(this.itemLabelProvider); listDescription.itemImageURLProvider = Objects.requireNonNull(this.itemImageURLProvider); + listDescription.listItemActionProvider = Objects.requireNonNull(this.listItemActionProvider); + listDescription.listItemActionTooltipProvider = Objects.requireNonNull(this.listItemActionTooltipProvider); + listDescription.listItemActionIconNameProvider = Objects.requireNonNull(this.listItemActionIconNameProvider); listDescription.diagnosticsProvider = Objects.requireNonNull(this.diagnosticsProvider); listDescription.kindProvider = Objects.requireNonNull(this.kindProvider); listDescription.messageProvider = Objects.requireNonNull(this.messageProvider); diff --git a/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/RepresentationsEventProcessorFactory.java b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/RepresentationsEventProcessorFactory.java new file mode 100644 index 0000000000..d7e0d3fbc9 --- /dev/null +++ b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/RepresentationsEventProcessorFactory.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 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 + *******************************************************************************/ +package org.eclipse.sirius.web.spring.collaborative.forms; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.web.core.api.IEditingContext; +import org.eclipse.sirius.web.core.api.IObjectService; +import org.eclipse.sirius.web.forms.description.FormDescription; +import org.eclipse.sirius.web.spring.collaborative.api.IRepresentationConfiguration; +import org.eclipse.sirius.web.spring.collaborative.api.IRepresentationEventProcessor; +import org.eclipse.sirius.web.spring.collaborative.api.IRepresentationEventProcessorFactory; +import org.eclipse.sirius.web.spring.collaborative.api.ISubscriptionManagerFactory; +import org.eclipse.sirius.web.spring.collaborative.forms.api.IFormEventHandler; +import org.eclipse.sirius.web.spring.collaborative.forms.api.IFormEventProcessor; +import org.eclipse.sirius.web.spring.collaborative.forms.api.IRepresentationsDescriptionProvider; +import org.eclipse.sirius.web.spring.collaborative.forms.api.IWidgetSubscriptionManagerFactory; +import org.eclipse.sirius.web.spring.collaborative.forms.api.RepresentationsConfiguration; +import org.springframework.stereotype.Service; + +/** + * Used to create the representations event processors. + * + * @author gcoutable + */ +@Service +public class RepresentationsEventProcessorFactory implements IRepresentationEventProcessorFactory { + + private final IRepresentationsDescriptionProvider representationsDescriptionProvider; + + private final IObjectService objectService; + + private final List formEventHandlers; + + private final ISubscriptionManagerFactory subscriptionManagerFactory; + + private final IWidgetSubscriptionManagerFactory widgetSubscriptionManagerFactory; + + public RepresentationsEventProcessorFactory(IRepresentationsDescriptionProvider representationsDescriptionProvider, IObjectService objectService, List formEventHandlers, + ISubscriptionManagerFactory subscriptionManagerFactory, IWidgetSubscriptionManagerFactory widgetSubscriptionManagerFactory) { + this.representationsDescriptionProvider = Objects.requireNonNull(representationsDescriptionProvider); + this.objectService = Objects.requireNonNull(objectService); + this.formEventHandlers = Objects.requireNonNull(formEventHandlers); + this.subscriptionManagerFactory = Objects.requireNonNull(subscriptionManagerFactory); + this.widgetSubscriptionManagerFactory = Objects.requireNonNull(widgetSubscriptionManagerFactory); + } + + @Override + public boolean canHandle(Class representationEventProcessorClass, IRepresentationConfiguration configuration) { + return IFormEventProcessor.class.isAssignableFrom(representationEventProcessorClass) && configuration instanceof RepresentationsConfiguration; + } + + @Override + public Optional createRepresentationEventProcessor(Class representationEventProcessorClass, IRepresentationConfiguration configuration, + IEditingContext editingContext) { + if (IFormEventProcessor.class.isAssignableFrom(representationEventProcessorClass) && configuration instanceof RepresentationsConfiguration) { + RepresentationsConfiguration representationsConfiguration = (RepresentationsConfiguration) configuration; + + FormDescription formDescription = this.representationsDescriptionProvider.getRepresentationsDescription(); + Optional optionalObject = this.objectService.getObject(editingContext, representationsConfiguration.getObjectId()); + if (optionalObject.isPresent()) { + Object object = optionalObject.get(); + if (formDescription != null) { + FormEventProcessor formEventProcessor = new FormEventProcessor(editingContext, formDescription, representationsConfiguration.getId(), object, this.formEventHandlers, + this.subscriptionManagerFactory.create(), this.widgetSubscriptionManagerFactory.create()); + + // @formatter:off + return Optional.of(formEventProcessor) + .filter(representationEventProcessorClass::isInstance) + .map(representationEventProcessorClass::cast); + // @formatter:on + } + } + } + + return Optional.empty(); + } + +} diff --git a/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/IRepresentationsDescriptionProvider.java b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/IRepresentationsDescriptionProvider.java new file mode 100644 index 0000000000..fbb875041a --- /dev/null +++ b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/IRepresentationsDescriptionProvider.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 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 + *******************************************************************************/ +package org.eclipse.sirius.web.spring.collaborative.forms.api; + +import org.eclipse.sirius.web.forms.description.FormDescription; + +/** + * Interface used to contribute the form description that contains all OCP representations. + * + * @author gcoutable + */ +public interface IRepresentationsDescriptionProvider { + FormDescription getRepresentationsDescription(); +} diff --git a/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/RepresentationsConfiguration.java b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/RepresentationsConfiguration.java new file mode 100644 index 0000000000..f2b414c812 --- /dev/null +++ b/backend/sirius-web-spring-collaborative-forms/src/main/java/org/eclipse/sirius/web/spring/collaborative/forms/api/RepresentationsConfiguration.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 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 + *******************************************************************************/ +package org.eclipse.sirius.web.spring.collaborative.forms.api; + +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.web.spring.collaborative.api.IRepresentationConfiguration; + +/** + * The configuration used to create the representations event processor. + * + * @author gcoutable + */ +public class RepresentationsConfiguration implements IRepresentationConfiguration { + + private static final String REPRESENTATIONS_PREFIX = "representations:"; //$NON-NLS-1$ + + private final String objectId; + + private UUID formId; + + public RepresentationsConfiguration(String objectId) { + this.objectId = Objects.requireNonNull(objectId); + this.formId = UUID.nameUUIDFromBytes((REPRESENTATIONS_PREFIX + objectId).getBytes()); + } + + @Override + public UUID getId() { + return this.formId; + } + + public String getObjectId() { + return this.objectId; + } + +} diff --git a/frontend/src/form/Form.types.ts b/frontend/src/form/Form.types.ts index e92a97c091..a1a2baa260 100644 --- a/frontend/src/form/Form.types.ts +++ b/frontend/src/form/Form.types.ts @@ -99,4 +99,10 @@ export interface ListItem { id: string; label: string; imageURL: string; + action: ListItemAction; +} + +export interface ListItemAction { + tooltip: string; + iconName: 'DeleteIcon' | 'CheckIcon'; } diff --git a/frontend/src/form/FormEventFragments.types.ts b/frontend/src/form/FormEventFragments.types.ts index 08acd26bc8..7dbc8a1fe4 100644 --- a/frontend/src/form/FormEventFragments.types.ts +++ b/frontend/src/form/FormEventFragments.types.ts @@ -129,4 +129,10 @@ export interface GQLListItem { id: string; label: string; imageURL: string; + action: GQLListItemAction; +} + +export interface GQLListItemAction { + tooltip: string; + iconName: string; } diff --git a/frontend/src/properties/propertysections/ListPropertySection.tsx b/frontend/src/properties/propertysections/ListPropertySection.tsx index cdb12d8bda..c9c7a898ac 100644 --- a/frontend/src/properties/propertysections/ListPropertySection.tsx +++ b/frontend/src/properties/propertysections/ListPropertySection.tsx @@ -10,6 +10,7 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ +import { IconButton, Tooltip } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import { makeStyles } from '@material-ui/core/styles'; @@ -17,6 +18,7 @@ import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableRow from '@material-ui/core/TableRow'; +import DeleteIcon from '@material-ui/icons/Delete'; import { httpOrigin } from 'common/URL'; import { ListPropertySectionProps } from 'properties/propertysections/ListPropertySection.types'; import { PropertySectionLabel } from 'properties/propertysections/PropertySectionLabel'; @@ -40,7 +42,7 @@ const useListPropertySectionStyles = makeStyles((theme) => ({ }, })); -export const ListPropertySection = ({ widget, subscribers }: ListPropertySectionProps) => { +export const ListPropertySection = ({ widget, subscribers, readonly }: ListPropertySectionProps) => { const classes = useListPropertySectionStyles(); let items = widget.items; @@ -49,6 +51,10 @@ export const ListPropertySection = ({ widget, subscribers }: ListPropertySection id: 'none', imageURL: '', label: 'None', + action: { + tooltip: 'no Action', + iconName: 'CheckIcon', + }, }); } @@ -59,7 +65,7 @@ export const ListPropertySection = ({ widget, subscribers }: ListPropertySection {widget.items.map((item) => ( - + {item.imageURL ? ( + {getActionIcon(item.action.iconName, readonly)} ))} @@ -79,3 +86,22 @@ export const ListPropertySection = ({ widget, subscribers }: ListPropertySection ); }; + +const getActionIcon = (iconeName: string, disabled: boolean) => { + if (iconeName === 'DeleteIcon') { + return ( + + + + + + ); + } + return ( + + + + + + ); +}; diff --git a/frontend/src/representations/RepresentationsWebSocketContainer.tsx b/frontend/src/representations/RepresentationsWebSocketContainer.tsx new file mode 100644 index 0000000000..5d3e1ed954 --- /dev/null +++ b/frontend/src/representations/RepresentationsWebSocketContainer.tsx @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) 2021 Obeo and others. + * 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 { SubscriptionResult } from '@apollo/client'; +import { makeStyles, Snackbar, Typography } from '@material-ui/core'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import { useMachine } from '@xstate/react'; +import { List } from 'form/Form.types'; +import { GQLFormRefreshedEventPayload, GQLPropertiesEventSubscription } from 'form/FormEventFragments.types'; +import { PubSub } from 'graphql-subscriptions'; +import { Properties } from 'properties/Properties'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { RepresentationContext } from 'workbench/RepresentationContext'; +import { RepresentationsWebSocketContainerProps } from './RepresentationsWebSocketContainer.types'; +import { + HandleSubscriptionResultEvent, + HideToastEvent, + RepresentationsWebSocketContainerContext, + RepresentationsWebSocketContainerEvent, + representationsWebSocketContainerMachine, + SchemaValue, + SwitchSelectionEvent, +} from './RepresentationsWebSocketContainerMachine'; + +const useRepresentationsWebSocketContainerStyles = makeStyles((theme) => ({ + idle: { + padding: theme.spacing(1), + }, +})); + +export const RepresentationsWebSocketContainer = ({ + editingContextId, + selection, + readOnly, +}: RepresentationsWebSocketContainerProps) => { + const classes = useRepresentationsWebSocketContainerStyles(); + + const [{ value, context }, dispatch] = useMachine< + RepresentationsWebSocketContainerContext, + RepresentationsWebSocketContainerEvent + >(representationsWebSocketContainerMachine); + + const { toast, representationsWebSocketContainer } = value as SchemaValue; + const { currentSelection, form, subscribers, widgetSubscriptions, message } = context; + const { registry } = useContext(RepresentationContext); + + const pubSub = useMemo(() => new PubSub(), []); + + useEffect(() => { + if (currentSelection?.id !== selection?.id) { + const isRepresentation = registry.isRepresentation(selection.kind); + const switchSelectionEvent: SwitchSelectionEvent = { type: 'SWITCH_SELECTION', selection, isRepresentation }; + dispatch(switchSelectionEvent); + } + }, [currentSelection, registry, selection, dispatch]); + + // Temporary useEffect, to remove before commit + useEffect(() => { + if (representationsWebSocketContainer === 'idle') { + const representationsEvent: GQLFormRefreshedEventPayload = { + __typename: 'FormRefreshedEventPayload', + id: 'customEventRefresh', + form: { + id: 'form', + label: 'Representation form', + pages: [ + { + id: 'page', + label: 'representation page', + groups: [ + { + id: 'widget', + label: 'Representations Group', + widgets: [ + { + __typename: 'List', + id: 'representationList', + label: 'Representations', + diagnostics: [], + items: [ + { + id: 'item 1', + imageURL: '', + label: 'Representation 1', + action: { + tooltip: 'Delete Representation 1', + iconName: 'DeleteIcon', + }, + }, + { + id: 'item 2', + imageURL: '', + label: 'Representation 2', + action: { + tooltip: 'Delete Representation 2', + iconName: 'DeleteIcon', + }, + }, + { + id: 'item 3', + imageURL: '', + label: 'Representation 3', + action: { + tooltip: 'Delete Representation 3', + iconName: 'DeleteIcon', + }, + }, + ], + } as List, + ], + }, + ], + }, + ], + }, + }; + + pubSub.publish('FORM_REFRESHED', { + representationsEvent, + }); + } + }, [pubSub, representationsWebSocketContainer]); + + useEffect(() => { + pubSub + .asyncIterator(['FORM_REFRESHED']) + .next() + .then((value) => { + let result: SubscriptionResult = { + loading: false, + data: value.value, + }; + const handleDataEvent: HandleSubscriptionResultEvent = { + type: 'HANDLE_SUBSCRIPTION_RESULT', + result, + }; + dispatch(handleDataEvent); + }); + }, [dispatch, pubSub]); + + let content = null; + if (!selection || representationsWebSocketContainer === 'unsupportedSelection') { + content = ( +
+ No object selected +
+ ); + } + if ((representationsWebSocketContainer === 'idle' && form) || representationsWebSocketContainer === 'ready') { + content = ( + + ); + } + + return ( + <> + {content} + dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)} + message={message} + action={ + dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)}> + + + } + data-testid="error" + /> + + ); +}; diff --git a/frontend/src/representations/RepresentationsWebSocketContainer.types.ts b/frontend/src/representations/RepresentationsWebSocketContainer.types.ts new file mode 100644 index 0000000000..bd02359368 --- /dev/null +++ b/frontend/src/representations/RepresentationsWebSocketContainer.types.ts @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2021 Obeo and others. + * 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 { Selection } from 'workbench/Workbench.types'; + +export interface RepresentationsWebSocketContainerProps { + editingContextId: string; + selection: Selection; + readOnly: boolean; +} diff --git a/frontend/src/representations/RepresentationsWebSocketContainerMachine.ts b/frontend/src/representations/RepresentationsWebSocketContainerMachine.ts new file mode 100644 index 0000000000..114c6016f4 --- /dev/null +++ b/frontend/src/representations/RepresentationsWebSocketContainerMachine.ts @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) 2021 Obeo and others. + * 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 { SubscriptionResult } from '@apollo/client'; +import { Form, Subscriber, WidgetSubscription } from 'form/Form.types'; +import { + GQLFormRefreshedEventPayload, + GQLPropertiesEventPayload, + GQLPropertiesEventSubscription, + GQLSubscribersUpdatedEventPayload, + GQLWidgetSubscriptionsUpdatedEventPayload, +} from 'form/FormEventFragments.types'; +import { v4 as uuid } from 'uuid'; +import { Selection } from 'workbench/Workbench.types'; +import { assign, Machine } from 'xstate'; + +export interface RepresentationsWebSocketContainerStateSchema { + states: { + toast: { + states: { + visible: {}; + hidden: {}; + }; + }; + representationsWebSocketContainer: { + states: { + empty: {}; + unsupportedSelection: {}; + idle: {}; + ready: {}; + complete: {}; + }; + }; + }; +} + +export type SchemaValue = { + toast: 'visible' | 'hidden'; + representationsWebSocketContainer: 'empty' | 'unsupportedSelection' | 'idle' | 'ready' | 'complete'; +}; + +export interface RepresentationsWebSocketContainerContext { + id: string; + currentSelection: Selection | null; + form: Form | null; + subscribers: Subscriber[]; + widgetSubscriptions: WidgetSubscription[]; + message: string | null; +} + +export type ShowToastEvent = { type: 'SHOW_TOAST'; message: string }; +export type HideToastEvent = { type: 'HIDE_TOAST' }; +export type SwitchSelectionEvent = { type: 'SWITCH_SELECTION'; selection: Selection; isRepresentation: boolean }; +export type HandleSubscriptionResultEvent = { + type: 'HANDLE_SUBSCRIPTION_RESULT'; + result: SubscriptionResult; +}; +export type HandleCompleteEvent = { type: 'HANDLE_COMPLETE' }; +export type RepresentationsWebSocketContainerEvent = + | SwitchSelectionEvent + | HandleSubscriptionResultEvent + | HandleCompleteEvent + | ShowToastEvent + | HideToastEvent; + +const isFormRefreshedEventPayload = (payload: GQLPropertiesEventPayload): payload is GQLFormRefreshedEventPayload => + payload.__typename === 'FormRefreshedEventPayload'; +const isSubscribersUpdatedEventPayload = ( + payload: GQLPropertiesEventPayload +): payload is GQLSubscribersUpdatedEventPayload => payload.__typename === 'SubscribersUpdatedEventPayload'; +const isWidgetSubscriptionsUpdatedEventPayload = ( + payload: GQLPropertiesEventPayload +): payload is GQLWidgetSubscriptionsUpdatedEventPayload => + payload.__typename == 'WidgetSubscriptionsUpdatedEventPayload'; + +export const representationsWebSocketContainerMachine = Machine< + RepresentationsWebSocketContainerContext, + RepresentationsWebSocketContainerStateSchema, + RepresentationsWebSocketContainerEvent +>( + { + type: 'parallel', + context: { + id: uuid(), + currentSelection: null, + form: null, + subscribers: [], + widgetSubscriptions: [], + message: null, + }, + states: { + toast: { + initial: 'hidden', + states: { + hidden: { + on: { + SHOW_TOAST: { + target: 'visible', + actions: 'setMessage', + }, + }, + }, + visible: { + on: { + HIDE_TOAST: { + target: 'hidden', + actions: 'clearMessage', + }, + }, + }, + }, + }, + representationsWebSocketContainer: { + initial: 'empty', + states: { + empty: { + on: { + SWITCH_SELECTION: [ + { + cond: 'isSelectionUnsupported', + target: 'unsupportedSelection', + actions: ['switchSelection', 'clearForm'], + }, + { + target: 'idle', + actions: 'switchSelection', + }, + ], + }, + }, + unsupportedSelection: { + on: { + SWITCH_SELECTION: [ + { + cond: 'isSelectionUnsupported', + target: 'unsupportedSelection', + actions: ['switchSelection', 'clearForm'], + }, + { + target: 'idle', + actions: 'switchSelection', + }, + ], + }, + }, + idle: { + on: { + SWITCH_SELECTION: [ + { + cond: 'isSelectionUnsupported', + target: 'unsupportedSelection', + actions: ['switchSelection', 'clearForm'], + }, + { + target: 'idle', + actions: 'switchSelection', + }, + ], + HANDLE_SUBSCRIPTION_RESULT: [ + { + cond: 'isFormRefreshedEventPayload', + target: 'ready', + actions: 'handleSubscriptionResult', + }, + { + target: 'idle', + actions: 'handleSubscriptionResult', + }, + ], + }, + }, + ready: { + on: { + SWITCH_SELECTION: [ + { + cond: 'isSelectionUnsupported', + target: 'unsupportedSelection', + actions: ['switchSelection', 'clearForm'], + }, + { + target: 'idle', + actions: 'switchSelection', + }, + ], + HANDLE_SUBSCRIPTION_RESULT: { + target: 'ready', + actions: 'handleSubscriptionResult', + }, + HANDLE_COMPLETE: { + target: 'complete', + }, + }, + }, + complete: { + on: { + SWITCH_SELECTION: [ + { + cond: 'isSelectionUnsupported', + target: 'unsupportedSelection', + actions: ['switchSelection', 'clearForm'], + }, + { + target: 'idle', + actions: 'switchSelection', + }, + ], + }, + }, + }, + }, + }, + }, + { + guards: { + isFormRefreshedEventPayload: (_, event) => { + const { result } = event as HandleSubscriptionResultEvent; + const { data } = result; + return isFormRefreshedEventPayload(data.propertiesEvent); + }, + isSelectionUnsupported: (_, event) => { + const { selection, isRepresentation } = event as SwitchSelectionEvent; + return !selection || isRepresentation || selection.kind === 'Unknown' || selection.kind === 'Document'; + }, + }, + actions: { + switchSelection: assign((_, event) => { + const { selection } = event as SwitchSelectionEvent; + return { id: uuid(), currentSelection: selection }; + }), + clearForm: assign((_, event) => { + return { form: null }; + }), + handleSubscriptionResult: assign((_, event) => { + const { result } = event as HandleSubscriptionResultEvent; + const { data } = result; + if (isFormRefreshedEventPayload(data.propertiesEvent)) { + const { form } = data.propertiesEvent; + return { form }; + } else if (isSubscribersUpdatedEventPayload(data.propertiesEvent)) { + const { subscribers } = data.propertiesEvent; + return { subscribers }; + } else if (isWidgetSubscriptionsUpdatedEventPayload(data.propertiesEvent)) { + const { widgetSubscriptions } = data.propertiesEvent; + return { widgetSubscriptions }; + } + return {}; + }), + setMessage: assign((_, event) => { + const { message } = event as ShowToastEvent; + return { message }; + }), + clearMessage: assign((_) => { + return { message: null }; + }), + }, + } +); diff --git a/frontend/src/workbench/RightSite.tsx b/frontend/src/workbench/RightSite.tsx index f84c6842df..99f81aa92c 100644 --- a/frontend/src/workbench/RightSite.tsx +++ b/frontend/src/workbench/RightSite.tsx @@ -18,6 +18,7 @@ import MuiCollapse from '@material-ui/core/Collapse'; import { makeStyles, withStyles } from '@material-ui/core/styles'; import { PropertiesWebSocketContainer } from 'properties/PropertiesWebSocketContainer'; import React from 'react'; +import { RepresentationsWebSocketContainer } from 'representations/RepresentationsWebSocketContainer'; import { RightSiteProps } from './RightSite.types'; const useSiteStyles = makeStyles((theme) => ({ @@ -78,12 +79,22 @@ export const RightSite = ({ editingContextId, selection, readOnly }: RightSitePr return (
- + Details + + Representations + + + +
); };