diff --git a/frontend/src/diagram/DiagramWebSocketContainer.tsx b/frontend/src/diagram/DiagramWebSocketContainer.tsx
index 234e62db631..c30d3be2cf3 100644
--- a/frontend/src/diagram/DiagramWebSocketContainer.tsx
+++ b/frontend/src/diagram/DiagramWebSocketContainer.tsx
@@ -70,7 +70,6 @@ const propTypes = {
kind: PropTypes.string.isRequired,
}),
setSelection: PropTypes.func.isRequired,
- setSubscribers: PropTypes.func.isRequired,
};
/**
@@ -227,7 +226,6 @@ export const DiagramWebSocketContainer = ({
readOnly,
selection,
setSelection,
- setSubscribers,
}: DiagramWebSocketContainerProps) => {
const diagramDomElement = useRef(null);
@@ -477,14 +475,6 @@ export const DiagramWebSocketContainer = ({
dispatch({ type: HANDLE_ERROR__ACTION, message: error });
}
- /**
- * Each time the list of subscribers is updated, this will trigger the listener used to display the
- * subscribers outside of this component.
- */
- useEffect(() => {
- setSubscribers(subscribers);
- }, [setSubscribers, subscribers]);
-
const onZoomIn = () => {
if (diagramServer) {
diagramServer.actionDispatcher.dispatch({ kind: ZOOM_IN_ACTION });
@@ -610,6 +600,7 @@ export const DiagramWebSocketContainer = ({
onFitToScreen={onFitToScreen}
setZoomLevel={setZoomLevel}
zoomLevel={zoomLevel}
+ subscribers={subscribers}
/>
{content}
diff --git a/frontend/src/diagram/DiagramWebSocketContainer.types.ts b/frontend/src/diagram/DiagramWebSocketContainer.types.ts
index 8fe40466d08..dfbf66a1871 100644
--- a/frontend/src/diagram/DiagramWebSocketContainer.types.ts
+++ b/frontend/src/diagram/DiagramWebSocketContainer.types.ts
@@ -10,7 +10,7 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-import { Selection, Subscriber } from 'views/edit-project/EditProjectView.types';
+import { Selection } from 'workbench/Workbench.types';
export type DiagramWebSocketContainerProps = {
projectId: string;
@@ -18,5 +18,8 @@ export type DiagramWebSocketContainerProps = {
readOnly: boolean;
selection: Selection;
setSelection: (newSelection: Selection) => void;
- setSubscribers: (newSubscribers: Subscriber[]) => void;
+};
+
+export type Subscriber = {
+ username: string;
};
diff --git a/frontend/src/diagram/Toolbar.module.css b/frontend/src/diagram/Toolbar.module.css
deleted file mode 100644
index e001b193719..00000000000
--- a/frontend/src/diagram/Toolbar.module.css
+++ /dev/null
@@ -1,56 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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
- *******************************************************************************/
-.toolbar {
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
- height: 32px;
-
- grid-column-start: 1;
- grid-column-end: 4;
- border-bottom: 1px solid var(--daintree-lighten-70);
-
- fill: none;
- stroke: var(--daintree);
-}
-
-.toolbar > * {
- height: 30px;
- width: 30px;
-}
-
-.toolbar > *:last-child {
- height: 30px;
- width: 70px;
-}
-
-.icon {
- fill: var(--daintree);
-}
-
-.icon:hover {
- background-color: var(--blue-lagoon-lighten-95);
-}
-
-.icon:focus {
- background-color: var(--blue-lagoon-lighten-90);
-}
-
-.separator {
- height: 24px;
- width: 1px;
- border-color: var(--daintree);
- border-left-style: solid;
- border-left-width: 1px;
- margin: 4px;
-}
diff --git a/frontend/src/diagram/Toolbar.tsx b/frontend/src/diagram/Toolbar.tsx
index 8003c16614c..88b65a5bbf5 100644
--- a/frontend/src/diagram/Toolbar.tsx
+++ b/frontend/src/diagram/Toolbar.tsx
@@ -10,27 +10,20 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-import { IconButton } from 'core/button/Button';
-import { Select } from 'core/select/Select';
-import { FitToScreen, Share, ZoomIn, ZoomOut } from 'icons';
+import Avatar from '@material-ui/core/Avatar';
+import FormControl from '@material-ui/core/FormControl';
+import IconButton from '@material-ui/core/IconButton';
+import MenuItem from '@material-ui/core/MenuItem';
+import Select from '@material-ui/core/Select';
+import { makeStyles } from '@material-ui/core/styles';
+import Tooltip from '@material-ui/core/Tooltip';
+import AspectRatioIcon from '@material-ui/icons/AspectRatio';
+import ShareIcon from '@material-ui/icons/Share';
+import ZoomInIcon from '@material-ui/icons/ZoomIn';
+import ZoomOutIcon from '@material-ui/icons/ZoomOut';
import { ShareDiagramModal } from 'modals/share-diagram/ShareDiagramModal';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
-import styles from './Toolbar.module.css';
-
-const zooms = [
- { id: '4', label: '400%' },
- { id: '2', label: '200%' },
- { id: '1.75', label: '175%' },
- { id: '1.5', label: '150%' },
- { id: '1.25', label: '125%' },
- { id: '1', label: '100%' },
- { id: '0.75', label: '75%' },
- { id: '0.5', label: '50%' },
- { id: '0.25', label: '25%' },
- { id: '0.1', label: '10%' },
- { id: '0.05', label: '5%' },
-];
const propTypes = {
onZoomIn: PropTypes.func.isRequired,
@@ -38,9 +31,44 @@ const propTypes = {
onFitToScreen: PropTypes.func.isRequired,
setZoomLevel: PropTypes.func.isRequired,
zoomLevel: PropTypes.string,
+ subscribers: PropTypes.array.isRequired,
};
-export const Toolbar = ({ onZoomIn, onZoomOut, onFitToScreen, setZoomLevel, zoomLevel }) => {
- const [state, setState] = useState({ modal: undefined, currentZoomLevel: undefined });
+
+const useToolbarStyles = makeStyles((theme) => ({
+ toolbar: {
+ display: 'flex',
+ flexDirection: 'row',
+ height: theme.spacing(4),
+ paddingLeft: theme.spacing(1),
+ paddingRight: theme.spacing(1),
+ borderBottomWidth: '1px',
+ borderBottomStyle: 'solid',
+ borderBottomColor: theme.palette.divider,
+ },
+ selectFormControl: {
+ minWidth: 70,
+ },
+ subscribers: {
+ marginLeft: 'auto',
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ '& > *': {
+ marginLeft: theme.spacing(0.5),
+ marginRight: theme.spacing(0.5),
+ },
+ },
+ avatar: {
+ fontSize: '1rem',
+ width: theme.spacing(3),
+ height: theme.spacing(3),
+ backgroundColor: theme.palette.primary.main,
+ },
+}));
+
+export const Toolbar = ({ onZoomIn, onZoomOut, onFitToScreen, setZoomLevel, zoomLevel, subscribers }) => {
+ const classes = useToolbarStyles();
+ const [state, setState] = useState({ modal: undefined, currentZoomLevel: zoomLevel });
const onShare = () => setState({ modal: 'ShareDiagramModal', currentZoomLevel: state.currentZoomLevel });
const closeModal = () => setState({ modal: undefined, currentZoomLevel: state.currentZoomLevel });
@@ -62,28 +90,52 @@ export const Toolbar = ({ onZoomIn, onZoomOut, onFitToScreen, setZoomLevel, zoom
}
return (
<>
-
-
-
+
+
+
+
+
+
-
-
-
+
+
-
-
+
+
-
-
+
+
-
+
+
+ {subscribers.map((subscriber) => (
+
+ {subscriber.username.substring(0, 1).toUpperCase()}
+
+ ))}
+
{modalElement}
>
diff --git a/frontend/src/diagram/reducer.ts b/frontend/src/diagram/reducer.ts
index 438d8b542b5..d1ff10a85c9 100644
--- a/frontend/src/diagram/reducer.ts
+++ b/frontend/src/diagram/reducer.ts
@@ -44,7 +44,7 @@ export const initialState = {
contextualPalette: undefined,
latestSelection: undefined,
newSelection: undefined,
- zoomLevel: undefined,
+ zoomLevel: '1',
subscribers: [],
message: undefined,
};
diff --git a/frontend/src/index.ts b/frontend/src/index.ts
index 8df27dc19c1..2503d2431f6 100644
--- a/frontend/src/index.ts
+++ b/frontend/src/index.ts
@@ -73,8 +73,6 @@ export * from './modals/new-root-object/NewRootObjectModal';
export * from './modals/rename-project/RenameProjectModal';
export * from './modals/share-diagram/ShareDiagramModal';
export * from './modals/upload-document/UploadDocumentModal';
-export * from './navbar/EditProjectNavbar/EditProjectNavbar';
-export * from './navbar/EditProjectNavbarContextMenu';
export * from './navbar/LoggedInNavbar';
export * from './navbar/LoggedOutNavbar';
export * from './navbar/Logo';
@@ -104,11 +102,8 @@ export * from './tree/Tree';
export * from './tree/TreeItem';
export * from './tree/TreeItemDiagramContextMenu';
export * from './tree/TreeItemObjectContextMenu';
-export * from './views/edit-project/EditProjectLoadedView';
+export * from './views/edit-project/EditProjectNavbar/EditProjectNavbar';
export * from './views/edit-project/EditProjectView';
-export * from './views/edit-project/OnboardArea';
-export * from './views/edit-project/RepresentationArea';
-export * from './views/edit-project/RepresentationNavigation';
export * from './views/ErrorView';
export * from './views/Footer';
export * from './views/FormContainer';
diff --git a/frontend/src/navbar/EditProjectNavbar/__tests__/reducer.test.ts b/frontend/src/navbar/EditProjectNavbar/__tests__/reducer.test.ts
deleted file mode 100644
index 506fa57f81d..00000000000
--- a/frontend/src/navbar/EditProjectNavbar/__tests__/reducer.test.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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 {
- EMPTY__STATE,
- CONTEXTUAL_MENU_DISPLAYED__STATE,
- MODAL_DISPLAYED__STATE,
- REDIRECT__STATE,
- HANDLE_SHOW_CONTEXT_MENU__ACTION,
- HANDLE_SHOW_MODAL__ACTION,
- HANDLE_REDIRECTING__ACTION,
- HANDLE_CLOSE_CONTEXT_MENU__ACTION,
- HANDLE_CLOSE_MODAL__ACTION,
-} from '../machine';
-import { initialState, reducer } from '../reducer';
-
-const contextualMenuDisplayedState = {
- viewState: CONTEXTUAL_MENU_DISPLAYED__STATE,
- to: null,
- modalDisplayed: null,
- x: 126,
- y: 250,
-};
-
-const modalDisplayedState = {
- viewState: MODAL_DISPLAYED__STATE,
- to: null,
- modalDisplayed: 'DeleteProject',
- x: 0,
- y: 0,
-};
-
-describe('EditProjectNavbar - reducer', () => {
- it('test the initial state', () => {
- const state = initialState;
-
- expect(state.viewState).toBe(EMPTY__STATE);
- expect(state.to).toBeNull();
- expect(state.modalDisplayed).toBeNull();
- expect(state.x).toBe(0);
- expect(state.y).toBe(0);
- });
-
- it('navigates to the context menu displayed state from empty state, when required', () => {
- const prevState = initialState;
-
- const action = { type: HANDLE_SHOW_CONTEXT_MENU__ACTION, x: 126, y: 250 };
- const state = reducer(prevState, action);
-
- expect(state.viewState).toBe(CONTEXTUAL_MENU_DISPLAYED__STATE);
- expect(state.to).toBeNull();
- expect(state.modalDisplayed).toBeNull();
- expect(state.x).toBe(126);
- expect(state.y).toBe(250);
- });
-
- it('navigates to the empty state when the contextual menu is closed', () => {
- const prevState = contextualMenuDisplayedState;
- const action = { type: HANDLE_CLOSE_CONTEXT_MENU__ACTION };
- const state = reducer(prevState, action);
-
- expect(state.viewState).toBe(EMPTY__STATE);
- expect(state.to).toBeNull();
- expect(state.modalDisplayed).toBeNull();
- expect(state.x).toBe(0);
- expect(state.y).toBe(0);
- });
-
- it('navigates to the modal displayed state when an action requiering a modal is choosed in the contextual menu', () => {
- const prevState = contextualMenuDisplayedState;
- const action = { type: HANDLE_SHOW_MODAL__ACTION, modalDisplayed: 'DeleteProject' };
- const state = reducer(prevState, action);
-
- expect(state.viewState).toBe(MODAL_DISPLAYED__STATE);
- expect(state.to).toBeNull();
- expect(state.modalDisplayed).toBe('DeleteProject');
- expect(state.x).toBe(0);
- expect(state.y).toBe(0);
- });
-
- it('navigates to the empty state when the modal is closed', () => {
- const prevState = modalDisplayedState;
- const action = { type: HANDLE_CLOSE_MODAL__ACTION };
- const state = reducer(prevState, action);
-
- expect(state.viewState).toBe(EMPTY__STATE);
- expect(state.to).toBeNull();
- expect(state.modalDisplayed).toBeNull();
- expect(state.x).toBe(0);
- expect(state.y).toBe(0);
- });
-
- it('navigates to the redirect state when the project is deleted using the delete project modal', () => {
- const prevState = modalDisplayedState;
- const action = { type: HANDLE_REDIRECTING__ACTION, to: '/projects' };
- const state = reducer(prevState, action);
-
- expect(state.viewState).toBe(REDIRECT__STATE);
- expect(state.to).toBe('/projects');
- expect(state.modalDisplayed).toBeNull();
- expect(state.x).toBe(0);
- expect(state.y).toBe(0);
- });
-});
diff --git a/frontend/src/properties/PropertiesWebSocketContainer.tsx b/frontend/src/properties/PropertiesWebSocketContainer.tsx
index ef12805783b..fd510a2fb19 100644
--- a/frontend/src/properties/PropertiesWebSocketContainer.tsx
+++ b/frontend/src/properties/PropertiesWebSocketContainer.tsx
@@ -108,10 +108,23 @@ const propertiesEventSubscription = gql`
/**
* Connect the Properties component to the GraphQL API over Web Socket.
*/
-export const PropertiesWebSocketContainer = ({ projectId, objectId }) => {
+export const PropertiesWebSocketContainer = ({ projectId, selection }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { viewState, form, displayedObjectId, subscribers, widgetSubscriptions, message } = state;
+ let objectId = null;
+ if (
+ selection &&
+ !(
+ selection.kind === 'Unknown' ||
+ selection.kind === 'Diagram' ||
+ selection.kind === 'Form' ||
+ selection.kind === 'Document'
+ )
+ ) {
+ objectId = selection.id;
+ }
+
/**
* Displays an other form if the selection indicates that we should display another properties view.
*/
diff --git a/frontend/src/views/edit-project/EditProjectLoadedView.tsx b/frontend/src/views/edit-project/EditProjectLoadedView.tsx
deleted file mode 100644
index 9bb4abf9aef..00000000000
--- a/frontend/src/views/edit-project/EditProjectLoadedView.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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 { forbidExtraProps } from 'airbnb-prop-types';
-import { HORIZONTAL, Panels, SECOND_PANEL } from 'core/panels/Panels';
-import { ExplorerWebSocketContainer } from 'explorer/ExplorerWebSocketContainer';
-import { EditProjectNavbar } from 'navbar/EditProjectNavbar/EditProjectNavbar';
-import PropTypes from 'prop-types';
-import { PropertiesWebSocketContainer } from 'properties/PropertiesWebSocketContainer';
-import React from 'react';
-import { RepresentationArea } from 'views/edit-project/RepresentationArea';
-import styles from './EditProjectLoadedView.module.css';
-
-const selectionPropType = PropTypes.shape(
- forbidExtraProps({
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- kind: PropTypes.string.isRequired,
- })
-);
-const propTypes = {
- projectId: PropTypes.string.isRequired,
- subscribers: PropTypes.array.isRequired,
- representations: PropTypes.array.isRequired,
- readOnly: PropTypes.bool.isRequired,
- selection: selectionPropType,
- displayedRepresentation: PropTypes.object,
- setSelection: PropTypes.func.isRequired,
- setSubscribers: PropTypes.func.isRequired,
-};
-export const EditProjectLoadedView = ({
- projectId,
- subscribers,
- representations,
- readOnly,
- selection,
- displayedRepresentation,
- setSelection,
- setSubscribers,
-}) => {
- const explorer = (
-
- );
-
- let representation = (
-
- );
- let objectId = undefined;
- if (
- selection &&
- !(
- selection.kind === 'Unknown' ||
- selection.kind === 'Diagram' ||
- selection.kind === 'Form' ||
- selection.kind === 'Document'
- )
- ) {
- objectId = selection.id;
- }
- const properties = ;
-
- const representationAndPropertiesPanel = (
-
- );
- return (
-
- );
-};
-EditProjectLoadedView.propTypes = propTypes;
diff --git a/frontend/src/navbar/EditProjectNavbar/EditProjectNavbar.module.css b/frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbar.module.css
similarity index 100%
rename from frontend/src/navbar/EditProjectNavbar/EditProjectNavbar.module.css
rename to frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbar.module.css
diff --git a/frontend/src/navbar/EditProjectNavbar/EditProjectNavbar.tsx b/frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbar.tsx
similarity index 79%
rename from frontend/src/navbar/EditProjectNavbar/EditProjectNavbar.tsx
rename to frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbar.tsx
index e36b16913be..c56ef9747dc 100644
--- a/frontend/src/navbar/EditProjectNavbar/EditProjectNavbar.tsx
+++ b/frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbar.tsx
@@ -10,24 +10,21 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-import { useAuth } from 'auth/useAuth';
import { useBranding } from 'common/BrandingContext';
import { IconButton } from 'core/button/Button';
import { LARGE, LIGHT } from 'core/subscriber/Subscriber';
import { Subscribers } from 'core/subscriber/Subscribers';
-import { Text } from 'core/text/Text';
import { More } from 'icons';
import { DeleteProjectModal } from 'modals/delete-project/DeleteProjectModal';
import { NewDocumentModal } from 'modals/new-document/NewDocumentModal';
import { RenameProjectModal } from 'modals/rename-project/RenameProjectModal';
import { UploadDocumentModal } from 'modals/upload-document/UploadDocumentModal';
-import { EditProjectNavbarContextMenu } from 'navbar/EditProjectNavbarContextMenu';
import { Logo } from 'navbar/Logo';
import { Title } from 'navbar/Title';
-import { useProject } from 'project/ProjectProvider';
import PropTypes from 'prop-types';
import React, { useReducer } from 'react';
import { Redirect } from 'react-router-dom';
+import { EditProjectNavbarContextMenu } from 'views/edit-project/EditProjectNavbar/EditProjectNavbarContextMenu';
import styles from './EditProjectNavbar.module.css';
import {
CONTEXTUAL_MENU_DISPLAYED__STATE,
@@ -52,14 +49,7 @@ const menuPositionDelta = {
const propTypes = {
subscribers: PropTypes.array.isRequired,
};
-export const EditProjectNavbar = ({ subscribers }) => {
- const {
- id,
- name,
- canEdit,
- owner: { username: projectOwner },
- } = useProject() as any;
- const { username } = useAuth() as any;
+export const EditProjectNavbar = ({ projectId, name, subscribers }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { userStatus } = useBranding();
const onMore = (event) => {
@@ -87,7 +77,7 @@ export const EditProjectNavbar = ({ subscribers }) => {
{
let modal = null;
if (modalDisplayed === 'CreateDocument') {
- modal = ;
+ modal = ;
} else if (modalDisplayed === 'UploadDocument') {
- modal = ;
+ modal = ;
} else if (modalDisplayed === 'RenameProject') {
modal = (
);
} else if (modalDisplayed === 'DeleteProject') {
- modal = ;
- }
- let accessDetails = undefined;
- if (!canEdit) {
- accessDetails = (
- {`View Only (owned by ${projectOwner})`}
- );
- } else if (projectOwner !== username) {
- accessDetails = (
- {`(owned by ${projectOwner})`}
- );
+ modal = ;
}
return (
<>
@@ -157,7 +135,6 @@ export const EditProjectNavbar = ({ subscribers }) => {
- {accessDetails}
diff --git a/frontend/src/navbar/EditProjectNavbarContextMenu.tsx b/frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbarContextMenu.tsx
similarity index 100%
rename from frontend/src/navbar/EditProjectNavbarContextMenu.tsx
rename to frontend/src/views/edit-project/EditProjectNavbar/EditProjectNavbarContextMenu.tsx
diff --git a/frontend/src/navbar/EditProjectNavbar/machine.ts b/frontend/src/views/edit-project/EditProjectNavbar/machine.ts
similarity index 100%
rename from frontend/src/navbar/EditProjectNavbar/machine.ts
rename to frontend/src/views/edit-project/EditProjectNavbar/machine.ts
diff --git a/frontend/src/navbar/EditProjectNavbar/reducer.ts b/frontend/src/views/edit-project/EditProjectNavbar/reducer.ts
similarity index 100%
rename from frontend/src/navbar/EditProjectNavbar/reducer.ts
rename to frontend/src/views/edit-project/EditProjectNavbar/reducer.ts
diff --git a/frontend/src/views/edit-project/EditProjectView.tsx b/frontend/src/views/edit-project/EditProjectView.tsx
index 13bc6ae0c2a..97ed5fd0576 100644
--- a/frontend/src/views/edit-project/EditProjectView.tsx
+++ b/frontend/src/views/edit-project/EditProjectView.tsx
@@ -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
@@ -10,31 +10,44 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-import { useLazyQuery, useSubscription } from '@apollo/client';
+import { useQuery } from '@apollo/client';
+import Grid from '@material-ui/core/Grid';
+import IconButton from '@material-ui/core/IconButton';
+import Snackbar from '@material-ui/core/Snackbar';
+import { makeStyles } from '@material-ui/core/styles';
+import Typography from '@material-ui/core/Typography';
+import CloseIcon from '@material-ui/icons/Close';
+import { useMachine } from '@xstate/react';
import gql from 'graphql-tag';
-import { useProject } from 'project/ProjectProvider';
-import React, { useCallback, useEffect, useReducer } from 'react';
+import React, { useEffect } from 'react';
import { generatePath, useHistory, useParams, useRouteMatch } from 'react-router-dom';
-import { EditProjectLoadedView } from 'views/edit-project/EditProjectLoadedView';
+import { EditProjectNavbar } from 'views/edit-project/EditProjectNavbar/EditProjectNavbar';
+import { GQLGetProjectQueryData, GQLGetProjectQueryVariables } from 'views/edit-project/EditProjectView.types';
import {
- HANDLE_FETCHED_PROJECT__ACTION,
- HANDLE_REPRESENTATION_LOADED__ACTION,
- HANDLE_REPRESENTATION_RENAMED__ACTION,
- HANDLE_SELECTION__ACTION,
- HANDLE_SUBSCRIBERS_UPDATED__ACTION,
- LOADING__STATE,
- PROJECT_AND_REPRESENTATION_LOADING__STATE,
- PROJECT_FETCHING_ERROR__STATE,
- PROJECT_NOT_FOUND__STATE,
-} from 'views/edit-project/machine';
-import { initialLoadingState, initialState, reducer } from 'views/edit-project/reducer';
-import { ErrorView } from 'views/ErrorView';
+ EditProjectViewContext,
+ EditProjectViewEvent,
+ editProjectViewMachine,
+ HandleFetchedProjectEvent,
+ HideToastEvent,
+ SchemaValue,
+ SelectRepresentationEvent,
+ ShowToastEvent,
+} from 'views/edit-project/EditProjectViewMachine';
+import { Workbench } from 'workbench/Workbench';
+import { Representation } from 'workbench/Workbench.types';
-const getRepresentationQuery = gql`
- query getRepresentation($projectId: ID!, $representationId: ID!) {
+const getProjectQuery = gql`
+ query getRepresentation($projectId: ID!, $representationId: ID!, $includeRepresentation: Boolean!) {
viewer {
project(projectId: $projectId) {
- representation(representationId: $representationId) {
+ id
+ name
+ visibility
+ accessLevel
+ currentEditingContext {
+ id
+ }
+ representation(representationId: $representationId) @include(if: $includeRepresentation) {
__typename
id
label
@@ -44,134 +57,115 @@ const getRepresentationQuery = gql`
}
`;
-const projectEventSubscription = gql`
- subscription projectEvent($input: ProjectEventInput!) {
- projectEvent(input: $input) {
- __typename
- ... on RepresentationRenamedEventPayload {
- representationId
- newLabel
- }
- }
- }
-`;
+const useEditProjectViewStyles = makeStyles((theme) => ({
+ editProjectView: {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100vh',
+ width: '100vw',
+ '& > *:nth-child(2)': {
+ flexGrow: 1,
+ },
+ },
+}));
export const EditProjectView = () => {
const history = useHistory();
const routeMatch = useRouteMatch();
const { projectId, representationId } = useParams();
- const [state, dispatch] = useReducer(reducer, representationId ? initialLoadingState : initialState);
- const { viewState, project, representations, displayedRepresentation, selection, subscribers, message } = state;
-
- const context = useProject() as any;
- let contextId;
- let canEdit = false;
- if (context) {
- contextId = context.id;
- canEdit = context.canEdit;
- }
-
- useEffect(() => {
- if (
- context &&
- context.id &&
- (viewState === LOADING__STATE || viewState === PROJECT_AND_REPRESENTATION_LOADING__STATE)
- ) {
- const action = {
- type: HANDLE_FETCHED_PROJECT__ACTION,
- response: {
- data: {
- viewer: {
- project: {
- id: context.id,
- name: context.name,
- visibility: context.visibility,
- accessLevel: context.accessLevel,
- },
- },
- },
- },
- };
- dispatch(action);
- }
- }, [context, viewState]);
+ const classes = useEditProjectViewStyles();
+ const [{ value, context }, dispatch] = useMachine
(
+ editProjectViewMachine
+ );
+ const { toast, editProjectView } = value as SchemaValue;
+ const { project, representation, message } = context;
- const [getRepresentation, { loading, data, error }] = useLazyQuery(getRepresentationQuery);
- useEffect(() => {
- if (representationId && project && !displayedRepresentation) {
- getRepresentation({ variables: { projectId: project.id, representationId } });
- }
- }, [project, representationId, displayedRepresentation, getRepresentation]);
+ const { loading, data, error } = useQuery(getProjectQuery, {
+ variables: {
+ projectId,
+ representationId: representationId ?? '',
+ includeRepresentation: !!representationId,
+ },
+ });
useEffect(() => {
- if (!loading && !error && data?.viewer?.project?.representation) {
- dispatch({ type: HANDLE_REPRESENTATION_LOADED__ACTION });
- const { id, label, __typename } = data.viewer.project.representation;
- const representation = { id, label, kind: __typename };
- const action = { type: HANDLE_SELECTION__ACTION, selection: representation };
- dispatch(action);
+ if (!loading) {
+ if (error) {
+ const showToastEvent: ShowToastEvent = {
+ type: 'SHOW_TOAST',
+ message: 'An unexpected error has occurred, please refresh the page',
+ };
+ dispatch(showToastEvent);
+ }
+ if (data) {
+ const fetchProjectEvent: HandleFetchedProjectEvent = { type: 'HANDLE_FETCHED_PROJECT', data };
+ dispatch(fetchProjectEvent);
+ }
}
- }, [loading, data, error]);
+ }, [loading, data, error, dispatch]);
useEffect(() => {
- if (displayedRepresentation && displayedRepresentation.id !== representationId) {
- const pathname = generatePath(routeMatch.path, { projectId, representationId: displayedRepresentation.id });
+ if (representation && representation.id !== representationId) {
+ const pathname = generatePath(routeMatch.path, { projectId, representationId: representation.id });
history.push({ pathname });
}
- }, [projectId, routeMatch, history, displayedRepresentation, representationId]);
-
- const setSelection = useCallback(
- (newSelectedObject) => {
- const action = { type: HANDLE_SELECTION__ACTION, selection: newSelectedObject };
- dispatch(action);
- },
- [dispatch]
- );
+ }, [projectId, routeMatch, history, representation, representationId]);
- useSubscription(projectEventSubscription, {
- variables: {
- input: {
- projectId,
- },
- },
- skip: !contextId,
- onSubscriptionData: ({ subscriptionData }) => {
- if (subscriptionData?.data?.projectEvent) {
- const { projectEvent } = subscriptionData.data;
- if (projectEvent.__typename === 'RepresentationRenamedEventPayload') {
- const action = { type: HANDLE_REPRESENTATION_RENAMED__ACTION, projectEvent };
- dispatch(action);
- }
+ let main = null;
+ if (editProjectView === 'loaded') {
+ const onRepresentationSelected = (representationSelected: Representation) => {
+ if (representationSelected.id !== representationId) {
+ const selectRepresentationEvent: SelectRepresentationEvent = {
+ type: 'SELECT_REPRESENTATION',
+ representation: representationSelected,
+ };
+ dispatch(selectRepresentationEvent);
}
- },
- });
-
- const setSubscribers = useCallback(
- (subscribers) => {
- const action = { type: HANDLE_SUBSCRIBERS_UPDATED__ACTION, subscribers };
- dispatch(action);
- },
- [dispatch]
- );
+ };
- if (!context) {
- return ;
- }
- if (viewState === LOADING__STATE || viewState === PROJECT_AND_REPRESENTATION_LOADING__STATE) {
- return ;
- }
- if (viewState === PROJECT_FETCHING_ERROR__STATE || viewState === PROJECT_NOT_FOUND__STATE) {
- return ;
+ main = (
+
+ );
+ } else if (editProjectView === 'missing') {
+ main = (
+
+
+ The project does not exist
+
+
+ );
}
+
return (
-
+ <>
+
+
+ {main}
+
+ dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)}
+ message={message}
+ action={
+ dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)}>
+
+
+ }
+ data-testid="error"
+ />
+ >
);
};
diff --git a/frontend/src/views/edit-project/EditProjectView.types.ts b/frontend/src/views/edit-project/EditProjectView.types.ts
index b084df7a40a..6a5340ebc92 100644
--- a/frontend/src/views/edit-project/EditProjectView.types.ts
+++ b/frontend/src/views/edit-project/EditProjectView.types.ts
@@ -10,12 +10,50 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-export type Selection = {
+export type Visibility = 'PUBLIC' | 'PRIVATE';
+export type AccessLevel = 'READ' | 'WRITE' | 'ADMIN';
+
+export type EditingContext = {
+ id: string;
+};
+
+export type Project = {
+ id: string;
+ name: string;
+ visibility: Visibility;
+ accessLevel: AccessLevel;
+ currentEditingContext: EditingContext;
+};
+
+export type GQLRepresentation = {
+ __typename: string;
id: string;
label: string;
- kind: string;
};
-export type Subscriber = {
- username: string;
+export type GQLEditingContext = {
+ id: string;
+};
+
+export type GQLProject = {
+ id: string;
+ name: string;
+ visibility: Visibility;
+ accessLevel: AccessLevel;
+ currentEditingContext: GQLEditingContext;
+ representation: GQLRepresentation | undefined;
+};
+
+export type GQLViewer = {
+ project: GQLProject;
+};
+
+export type GQLGetProjectQueryData = {
+ viewer: GQLViewer;
+};
+
+export type GQLGetProjectQueryVariables = {
+ projectId: string;
+ representationId: string;
+ includeRepresentation: boolean;
};
diff --git a/frontend/src/views/edit-project/EditProjectViewMachine.ts b/frontend/src/views/edit-project/EditProjectViewMachine.ts
new file mode 100644
index 00000000000..55887a72504
--- /dev/null
+++ b/frontend/src/views/edit-project/EditProjectViewMachine.ts
@@ -0,0 +1,157 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+import { GQLGetProjectQueryData, Project } from 'views/edit-project/EditProjectView.types';
+import { Representation } from 'workbench/Workbench.types';
+import { assign, Machine } from 'xstate';
+
+export interface EditProjectViewStateSchema {
+ states: {
+ toast: {
+ states: {
+ visible: {};
+ hidden: {};
+ };
+ };
+ editProjectView: {
+ states: {
+ loading: {};
+ loaded: {};
+ missing: {};
+ };
+ };
+ };
+}
+
+export type SchemaValue = {
+ toast: 'visible' | 'hidden';
+ editProjectView: 'loading' | 'loaded' | 'missing';
+};
+
+export interface EditProjectViewContext {
+ project: Project | null;
+ representation: Representation | null;
+ message: string | null;
+}
+
+export type ShowToastEvent = { type: 'SHOW_TOAST'; message: string };
+export type HideToastEvent = { type: 'HIDE_TOAST' };
+export type HandleFetchedProjectEvent = { type: 'HANDLE_FETCHED_PROJECT'; data: GQLGetProjectQueryData };
+export type SelectRepresentationEvent = { type: 'SELECT_REPRESENTATION'; representation: Representation };
+export type EditProjectViewEvent =
+ | HandleFetchedProjectEvent
+ | SelectRepresentationEvent
+ | ShowToastEvent
+ | HideToastEvent;
+
+export const editProjectViewMachine = Machine(
+ {
+ type: 'parallel',
+ context: {
+ project: null,
+ representation: null,
+ message: null,
+ },
+ states: {
+ toast: {
+ initial: 'hidden',
+ states: {
+ hidden: {
+ on: {
+ SHOW_TOAST: {
+ target: 'visible',
+ actions: 'setMessage',
+ },
+ },
+ },
+ visible: {
+ on: {
+ HIDE_TOAST: {
+ target: 'hidden',
+ actions: 'clearMessage',
+ },
+ },
+ },
+ },
+ },
+ editProjectView: {
+ initial: 'loading',
+ states: {
+ loading: {
+ on: {
+ HANDLE_FETCHED_PROJECT: [
+ {
+ cond: 'isMissing',
+ target: 'missing',
+ },
+ {
+ target: 'loaded',
+ actions: 'updateProject',
+ },
+ ],
+ },
+ },
+ loaded: {
+ type: 'final',
+ on: {
+ SELECT_REPRESENTATION: {
+ target: 'loaded',
+ actions: 'selectRepresentation',
+ },
+ },
+ },
+ missing: {
+ type: 'final',
+ },
+ },
+ },
+ },
+ },
+ {
+ guards: {
+ isMissing: (_, event) => {
+ const { data } = event as HandleFetchedProjectEvent;
+ return !data.viewer.project;
+ },
+ },
+ actions: {
+ updateProject: assign((_, event) => {
+ const { data } = event as HandleFetchedProjectEvent;
+ const { project: gQLProject } = data.viewer;
+ const { id, name, visibility, accessLevel, currentEditingContext } = gQLProject;
+ const project = { id, name, visibility, accessLevel, currentEditingContext: { id: currentEditingContext.id } };
+
+ let representation: Representation = null;
+ if (gQLProject.representation) {
+ representation = {
+ id: gQLProject.representation.id,
+ label: gQLProject.representation.label,
+ kind: gQLProject.representation.__typename,
+ };
+ }
+
+ return { project, representation };
+ }),
+ selectRepresentation: assign((_, event) => {
+ const { representation } = event as SelectRepresentationEvent;
+ return { representation };
+ }),
+ setMessage: assign((_, event) => {
+ const { message } = event as ShowToastEvent;
+ return { message };
+ }),
+ clearMessage: assign((_) => {
+ return { message: null };
+ }),
+ },
+ }
+);
diff --git a/frontend/src/views/edit-project/__tests__/reducer.test.ts b/frontend/src/views/edit-project/__tests__/reducer.test.ts
deleted file mode 100644
index 29d45675869..00000000000
--- a/frontend/src/views/edit-project/__tests__/reducer.test.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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 {
- LOADING__STATE,
- PROJECT_NOT_FOUND__STATE,
- PROJECT_FETCHING_ERROR__STATE,
- PROJECT_LOADED__STATE,
- PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- HANDLE_FETCHED_PROJECT__ACTION,
- HANDLE_SELECTION__ACTION,
- HANDLE_REPRESENTATION_RENAMED__ACTION,
- HANDLE_SUBSCRIBERS_UPDATED__ACTION,
-} from '../machine';
-import { initialState, reducer } from '../reducer';
-
-const FIRST_OBJECT_ID = 'First Object';
-const FIRST_OBJECT = { id: FIRST_OBJECT_ID, label: 'First object', kind: 'Object' };
-const SECOND_OBJECT_ID = 'Second Object';
-const SECOND_OBJECT = { id: SECOND_OBJECT_ID, label: 'Second object', kind: 'Object' };
-const FIRST_DIAGRAM_ID = 'First Diagram';
-const FIRST_DIAGRAM = { id: FIRST_DIAGRAM_ID, label: 'First diagram', kind: 'Diagram' };
-const SECOND_DIAGRAM_ID = 'Second Diagram';
-const SECOND_DIAGRAM = { id: SECOND_DIAGRAM_ID, label: 'Second diagram', kind: 'Diagram' };
-
-const projectLoadedState = {
- viewState: PROJECT_LOADED__STATE,
- project: {
- id: '',
- name: '',
- },
- representations: [],
- displayedRepresentation: undefined,
- selection: undefined,
- subscribers: [],
- message: undefined,
-};
-
-const projectLoadedAndrepresentationDisplayedState = {
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: {
- id: '',
- name: '',
- },
- representations: [FIRST_DIAGRAM],
- displayedRepresentation: FIRST_DIAGRAM,
- selection: FIRST_DIAGRAM,
- subscribers: [],
- message: undefined,
-};
-
-const objectSelectedState = {
- viewState: PROJECT_LOADED__STATE,
- project: {
- id: '',
- name: '',
- },
- representations: [],
- displayedRepresentation: undefined,
- selection: FIRST_OBJECT,
- subscribers: [],
- message: undefined,
-};
-
-const representationAndObjectSelectedState = {
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: {
- id: '',
- name: '',
- },
- representations: [FIRST_DIAGRAM],
- displayedRepresentation: FIRST_DIAGRAM,
- selection: FIRST_OBJECT,
- subscribers: [],
- message: undefined,
-};
-describe('EditProjectView - reducer', () => {
- it('navigates to the empty project loaded state', () => {
- expect(initialState).toStrictEqual({
- viewState: LOADING__STATE,
- project: undefined,
- representations: [],
- selection: undefined,
- displayedRepresentation: undefined,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the project not found state after loading a non-existing project', () => {
- const response = {
- data: {
- viewer: {},
- },
- };
-
- const prevState = initialState;
- const action = { type: HANDLE_FETCHED_PROJECT__ACTION, response };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_NOT_FOUND__STATE,
- message: 'The project requested does not exist',
- });
- });
-
- it('navigates to the project fetching error state after an unexpected error during the loading', () => {
- const prevState = initialState;
- const response = {
- errors: [
- {
- message: 'Unexpected error',
- location: [
- {
- line: 11,
- column: 8,
- },
- ],
- extensions: {
- classification: 'DataFetchingError',
- },
- },
- ],
- };
-
- const action = { type: HANDLE_FETCHED_PROJECT__ACTION, response };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_FETCHING_ERROR__STATE,
- message: 'An error has occurred while fetching the project. Please contact your administrator.',
- });
- });
-
- it('navigates to the project loaded state after loading the project', () => {
- const prevState = initialState;
- const response = {
- data: {
- viewer: {
- project: {
- id: '',
- name: '',
- },
- },
- },
- };
-
- const action = { type: HANDLE_FETCHED_PROJECT__ACTION, response };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED__STATE,
- project: response.data.viewer.project,
- representations: [],
- displayedRepresentation: undefined,
- selection: undefined,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the object selected state after selecting an object', () => {
- const prevState = projectLoadedState;
- const action = { type: HANDLE_SELECTION__ACTION, selection: FIRST_OBJECT };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED__STATE,
- project: projectLoadedState.project,
- representations: [],
- displayedRepresentation: undefined,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the projet loaded state after renaming a representation', () => {
- const prevState = projectLoadedState;
- const action = {
- type: HANDLE_REPRESENTATION_RENAMED__ACTION,
- projectEvent: { representationId: FIRST_DIAGRAM_ID, newLabel: '' },
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED__STATE,
- project: projectLoadedState.project,
- representations: [],
- displayedRepresentation: undefined,
- selection: projectLoadedState.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the representation displayed state after selecting an object', () => {
- const prevState = projectLoadedState;
- const action = {
- type: HANDLE_SELECTION__ACTION,
- selection: FIRST_DIAGRAM,
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: projectLoadedState.project,
- representations: [action.selection],
- displayedRepresentation: action.selection,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('stays to the project loaded state after renaming a representation', () => {
- const prevState = projectLoadedState;
- const action = {
- type: HANDLE_REPRESENTATION_RENAMED__ACTION,
- projectEvent: { representationId: FIRST_DIAGRAM_ID, newLabel: '' },
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: projectLoadedState.viewState,
- project: projectLoadedState.project,
- representations: projectLoadedState.representations,
- displayedRepresentation: projectLoadedState.displayedRepresentation,
- selection: projectLoadedState.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('stays to the project loaded and representation displayed state after renaming a representation', () => {
- const prevState = projectLoadedAndrepresentationDisplayedState;
- const action = {
- type: HANDLE_REPRESENTATION_RENAMED__ACTION,
- projectEvent: { representationId: FIRST_DIAGRAM_ID, newLabel: '' },
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: projectLoadedAndrepresentationDisplayedState.viewState,
- project: projectLoadedAndrepresentationDisplayedState.project,
- representations: projectLoadedAndrepresentationDisplayedState.representations,
- displayedRepresentation: projectLoadedAndrepresentationDisplayedState.displayedRepresentation,
- selection: projectLoadedAndrepresentationDisplayedState.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the representation and object selected state after selecting a representation', () => {
- const prevState = objectSelectedState;
- const action = {
- type: HANDLE_SELECTION__ACTION,
- selection: SECOND_DIAGRAM,
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: objectSelectedState.project,
- representations: [action.selection],
- displayedRepresentation: action.selection,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates to the representation and object selected state after selecting an object', () => {
- const prevState = projectLoadedAndrepresentationDisplayedState;
- const action = { type: HANDLE_SELECTION__ACTION, selection: SECOND_OBJECT };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: projectLoadedAndrepresentationDisplayedState.project,
- representations: projectLoadedAndrepresentationDisplayedState.representations,
- displayedRepresentation: projectLoadedAndrepresentationDisplayedState.displayedRepresentation,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates back to the representation and object selected state after selecting another representation', () => {
- const prevState = representationAndObjectSelectedState;
- const action = {
- type: HANDLE_SELECTION__ACTION,
- selection: SECOND_DIAGRAM,
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: representationAndObjectSelectedState.project,
- representations: [...representationAndObjectSelectedState.representations, action.selection],
- displayedRepresentation: action.selection,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates back to the representation and object selected state after selecting another object', () => {
- const prevState = representationAndObjectSelectedState;
- const action = { type: HANDLE_SELECTION__ACTION, selection: SECOND_OBJECT };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: representationAndObjectSelectedState.project,
- representations: representationAndObjectSelectedState.representations,
- displayedRepresentation: representationAndObjectSelectedState.displayedRepresentation,
- selection: action.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('navigates back to the project loaded and representation displayed state after renaming another representation', () => {
- const prevState = projectLoadedAndrepresentationDisplayedState;
- const action = {
- type: HANDLE_REPRESENTATION_RENAMED__ACTION,
- projectEvent: { representationId: SECOND_DIAGRAM_ID, newLabel: '' },
- };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: projectLoadedAndrepresentationDisplayedState.project,
- representations: projectLoadedAndrepresentationDisplayedState.representations,
- displayedRepresentation: projectLoadedAndrepresentationDisplayedState.displayedRepresentation,
- selection: projectLoadedAndrepresentationDisplayedState.selection,
- subscribers: [],
- message: undefined,
- });
- });
-
- it('updates the list of subscribers', () => {
- const prevState = representationAndObjectSelectedState;
- const subscribers = [{ username: 'jdoe' }];
- const action = { type: HANDLE_SUBSCRIBERS_UPDATED__ACTION, subscribers };
- const state = reducer(prevState, action);
-
- expect(state).toStrictEqual({
- viewState: PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- project: representationAndObjectSelectedState.project,
- representations: representationAndObjectSelectedState.representations,
- displayedRepresentation: representationAndObjectSelectedState.displayedRepresentation,
- selection: representationAndObjectSelectedState.selection,
- subscribers,
- message: undefined,
- });
- });
-});
diff --git a/frontend/src/views/edit-project/machine.tsx b/frontend/src/views/edit-project/machine.tsx
deleted file mode 100644
index 31aa88480dd..00000000000
--- a/frontend/src/views/edit-project/machine.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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 LOADING__STATE = 'LOADING__STATE';
-export const PROJECT_NOT_FOUND__STATE = 'PROJECT_NOT_FOUND__STATE';
-export const PROJECT_FETCHING_ERROR__STATE = 'PROJECT_FETCHING_ERROR__STATE';
-export const PROJECT_LOADED__STATE = 'PROJECT_LOADED__STATE';
-export const PROJECT_AND_REPRESENTATION_LOADING__STATE = 'PROJECT_AND_REPRESENTATION_LOADING__STATE';
-export const PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE = 'PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE';
-
-export const HANDLE_FETCHED_PROJECT__ACTION = 'HANDLE_FETCHED_PROJECT__ACTION';
-export const HANDLE_SELECTION__ACTION = 'HANDLE_SELECTION__ACTION';
-export const HANDLE_REPRESENTATION_RENAMED__ACTION = 'HANDLE_REPRESENTATION_RENAMED__ACTION';
-export const HANDLE_REPRESENTATION_LOADED__ACTION = 'HANDLE_REPRESENTATION_LOADED__ACTION';
-export const HANDLE_SUBSCRIBERS_UPDATED__ACTION = 'HANDLE_SUBSCRIBERS_UPDATED__ACTION';
-
-export const machine = {
- LOADING__STATE: {
- HANDLE_FETCHED_PROJECT__ACTION: [PROJECT_NOT_FOUND__STATE, PROJECT_FETCHING_ERROR__STATE, PROJECT_LOADED__STATE],
- },
- PROJECT_NOT_FOUND__STATE: {},
- PROJECT_FETCHING_ERROR__STATE: {},
- PROJECT_LOADED__STATE: {
- HANDLE_SELECTION__ACTION: [PROJECT_LOADED__STATE, PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- HANDLE_REPRESENTATION_RENAMED__ACTION: [PROJECT_LOADED__STATE, PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- },
- PROJECT_AND_REPRESENTATION_LOADING__STATE: {
- HANDLE_FETCHED_PROJECT__ACTION: [
- PROJECT_NOT_FOUND__STATE,
- PROJECT_FETCHING_ERROR__STATE,
- PROJECT_AND_REPRESENTATION_LOADING__STATE,
- ],
- HANDLE_REPRESENTATION_LOADED__ACTION: [PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- },
- PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE: {
- HANDLE_SELECTION__ACTION: [PROJECT_LOADED__STATE, PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- HANDLE_REPRESENTATION_RENAMED__ACTION: [PROJECT_LOADED__STATE, PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- HANDLE_SUBSCRIBERS_UPDATED__ACTION: [PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE],
- },
-};
diff --git a/frontend/src/views/edit-project/reducer.tsx b/frontend/src/views/edit-project/reducer.tsx
deleted file mode 100644
index 26843d63273..00000000000
--- a/frontend/src/views/edit-project/reducer.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2019, 2020 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 {
- machine,
- LOADING__STATE,
- PROJECT_NOT_FOUND__STATE,
- PROJECT_FETCHING_ERROR__STATE,
- PROJECT_LOADED__STATE,
- PROJECT_AND_REPRESENTATION_LOADING__STATE,
- PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE,
- HANDLE_FETCHED_PROJECT__ACTION,
- HANDLE_SELECTION__ACTION,
- HANDLE_REPRESENTATION_RENAMED__ACTION,
- HANDLE_REPRESENTATION_LOADED__ACTION,
- HANDLE_SUBSCRIBERS_UPDATED__ACTION,
-} from './machine';
-
-export const initialState = {
- viewState: LOADING__STATE,
- project: undefined,
- representations: [],
- selection: undefined,
- displayedRepresentation: undefined,
- subscribers: [],
- message: undefined,
-};
-
-export const initialLoadingState = {
- viewState: PROJECT_AND_REPRESENTATION_LOADING__STATE,
- project: undefined,
- representations: [],
- selection: undefined,
- displayedRepresentation: undefined,
- subscribers: [],
- message: undefined,
-};
-
-export const reducer = (prevState, action) => {
- const supportedActions = machine[prevState.viewState];
- if (!supportedActions[action.type]) {
- console.error(`The state ${prevState.viewState} does not support the action ${action.type}`);
- }
-
- let state = prevState;
- switch (action.type) {
- case HANDLE_FETCHED_PROJECT__ACTION:
- state = handleFetchedProjectAction(prevState, action);
- break;
- case HANDLE_SELECTION__ACTION:
- state = handleSelectionAction(prevState, action);
- break;
- case HANDLE_REPRESENTATION_RENAMED__ACTION:
- state = handleRepresentationRenamedAction(prevState, action);
- break;
- case HANDLE_REPRESENTATION_LOADED__ACTION:
- state = handleRepresentationLoadedAction(prevState, action);
- break;
- case HANDLE_SUBSCRIBERS_UPDATED__ACTION:
- state = handleSubscribersUpdated(prevState, action);
- break;
- default:
- state = prevState;
- }
-
- const newSupportedStates = supportedActions[action.type];
- if (!newSupportedStates || newSupportedStates.indexOf(state.viewState) === -1) {
- console.error(`The state ${state.viewState} should not be accessible with the action ${action.type}`);
- }
- return state;
-};
-
-const handleFetchedProjectAction = (prevState, action) => {
- const { viewState, representations, selection, displayedRepresentation, subscribers, message } = prevState;
- const { response } = action;
-
- let state = undefined;
- if (response.errors) {
- const message = 'An error has occurred while fetching the project. Please contact your administrator.';
- state = { viewState: PROJECT_FETCHING_ERROR__STATE, message };
- } else if (response.data.viewer.project) {
- const { project } = response.data.viewer;
- state = {
- viewState:
- viewState === PROJECT_AND_REPRESENTATION_LOADING__STATE
- ? PROJECT_AND_REPRESENTATION_LOADING__STATE
- : PROJECT_LOADED__STATE,
- project,
- representations,
- selection,
- displayedRepresentation,
- subscribers,
- message,
- };
- } else {
- const message = 'The project requested does not exist';
- state = { viewState: PROJECT_NOT_FOUND__STATE, message };
- }
-
- return state;
-};
-
-const handleSelectionAction = (prevState, action) => {
- const { selection } = action;
- const { viewState, project, representations, displayedRepresentation, subscribers, message } = prevState;
-
- let newRepresentations;
- let newDisplayedRepresentation;
- let newViewState = null;
-
- if (selection?.kind === 'Diagram' || selection?.kind === 'Form') {
- newViewState = PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE;
- newDisplayedRepresentation = selection;
- newRepresentations = [...representations];
- const selectedRepresentation = representations.find((representation) => selection.id === representation.id);
- if (!selectedRepresentation) {
- newRepresentations = [...representations, selection];
- } else {
- newRepresentations = representations;
- }
- newViewState = PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE;
- } else {
- // Keep existing representations & displayedRepresentation
- newRepresentations = representations;
- newDisplayedRepresentation = displayedRepresentation;
- if (
- viewState === PROJECT_LOADED__STATE ||
- viewState === PROJECT_AND_REPRESENTATION_LOADING__STATE ||
- viewState === PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE
- ) {
- newViewState = viewState;
- } else {
- console.error(
- 'Invalid state, the viewState must be PROJECT_LOADED__STATE or PROJECT_AND_REPRESENTATION_LOADING__STATE or PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE'
- );
- }
- }
-
- return {
- viewState: newViewState,
- project,
- representations: newRepresentations,
- displayedRepresentation: newDisplayedRepresentation,
- selection,
- subscribers,
- message,
- };
-};
-
-const handleRepresentationRenamedAction = (prevState, action) => {
- const { viewState, project, representations, displayedRepresentation, selection, subscribers, message } = prevState;
- const { projectEvent } = action;
- const { representationId, newLabel } = projectEvent;
- let representationToRename = representations.find((r) => r.id === representationId);
- if (representationToRename) {
- representationToRename.label = newLabel;
- }
-
- return {
- viewState,
- project,
- representations,
- displayedRepresentation,
- selection,
- subscribers,
- message,
- };
-};
-
-const handleRepresentationLoadedAction = (prevState, action) => {
- const { viewState, project, representations, displayedRepresentation, selection, subscribers, message } = prevState;
-
- let newViewState = null;
-
- if (viewState === PROJECT_AND_REPRESENTATION_LOADING__STATE) {
- newViewState = PROJECT_LOADED_AND_REPRESENTATION_DISPLAYED__STATE;
- } else {
- console.error('Invalid state, the viewState must be PROJECT_AND_REPRESENTATION_LOADING__STATE');
- }
-
- return {
- viewState: newViewState,
- project,
- representations,
- displayedRepresentation,
- selection,
- subscribers,
- message,
- };
-};
-
-const handleSubscribersUpdated = (prevState, action) => {
- const { subscribers } = action;
- const { viewState, project, representations, selection, displayedRepresentation, message } = prevState;
-
- return {
- viewState,
- project,
- representations,
- selection,
- displayedRepresentation,
- subscribers,
- message,
- };
-};
diff --git a/frontend/src/views/edit-project/OnboardArea.module.css b/frontend/src/workbench/OnboardArea.module.css
similarity index 100%
rename from frontend/src/views/edit-project/OnboardArea.module.css
rename to frontend/src/workbench/OnboardArea.module.css
diff --git a/frontend/src/views/edit-project/OnboardArea.tsx b/frontend/src/workbench/OnboardArea.tsx
similarity index 100%
rename from frontend/src/views/edit-project/OnboardArea.tsx
rename to frontend/src/workbench/OnboardArea.tsx
diff --git a/frontend/src/views/edit-project/RepresentationArea.module.css b/frontend/src/workbench/RepresentationArea.module.css
similarity index 100%
rename from frontend/src/views/edit-project/RepresentationArea.module.css
rename to frontend/src/workbench/RepresentationArea.module.css
diff --git a/frontend/src/views/edit-project/RepresentationArea.tsx b/frontend/src/workbench/RepresentationArea.tsx
similarity index 83%
rename from frontend/src/views/edit-project/RepresentationArea.tsx
rename to frontend/src/workbench/RepresentationArea.tsx
index 33bc5da7881..2126b22a0a8 100644
--- a/frontend/src/views/edit-project/RepresentationArea.tsx
+++ b/frontend/src/workbench/RepresentationArea.tsx
@@ -15,8 +15,7 @@ import { DiagramWebSocketContainer } from 'diagram/DiagramWebSocketContainer';
import { FormWebSocketContainer } from 'form/FormWebSocketContainer';
import PropTypes from 'prop-types';
import React from 'react';
-import { RepresentationNavigation } from 'views/edit-project/RepresentationNavigation';
-import { OnboardArea } from './OnboardArea';
+import { RepresentationNavigation } from 'workbench/RepresentationNavigation';
import styles from './RepresentationArea.module.css';
const propTypes = {
@@ -26,7 +25,6 @@ const propTypes = {
selection: PropTypes.object,
displayedRepresentation: PropTypes.object,
setSelection: PropTypes.func.isRequired,
- setSubscribers: PropTypes.func.isRequired,
};
export const RepresentationArea = ({
projectId,
@@ -35,12 +33,9 @@ export const RepresentationArea = ({
selection,
displayedRepresentation,
setSelection,
- setSubscribers,
}) => {
let content;
- if (!displayedRepresentation) {
- content = ;
- } else if (displayedRepresentation.kind === 'Diagram') {
+ if (displayedRepresentation.kind === 'Diagram') {
content = (
);
} else if (displayedRepresentation.kind === 'Form') {
diff --git a/frontend/src/views/edit-project/RepresentationNavigation.module.css b/frontend/src/workbench/RepresentationNavigation.module.css
similarity index 100%
rename from frontend/src/views/edit-project/RepresentationNavigation.module.css
rename to frontend/src/workbench/RepresentationNavigation.module.css
diff --git a/frontend/src/views/edit-project/RepresentationNavigation.tsx b/frontend/src/workbench/RepresentationNavigation.tsx
similarity index 90%
rename from frontend/src/views/edit-project/RepresentationNavigation.tsx
rename to frontend/src/workbench/RepresentationNavigation.tsx
index 4e22e3f2412..11e7a7301cd 100644
--- a/frontend/src/views/edit-project/RepresentationNavigation.tsx
+++ b/frontend/src/workbench/RepresentationNavigation.tsx
@@ -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
@@ -10,7 +10,7 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-import { Text } from 'core/text/Text';
+import Typography from '@material-ui/core/Typography';
import PropTypes from 'prop-types';
import React from 'react';
import styles from './RepresentationNavigation.module.css';
@@ -20,6 +20,7 @@ const propTypes = {
displayedRepresentation: PropTypes.object,
setSelection: PropTypes.func.isRequired,
};
+
export const RepresentationNavigation = ({ representations, displayedRepresentation, setSelection }) => {
return (
@@ -37,7 +38,7 @@ export const RepresentationNavigation = ({ representations, displayedRepresentat
onClick={() => setSelection({ id, label, kind })}
data-testid={`representation-tab-${label}`}
data-testselected={isSelected}>
- {label}
+ {label}
);
})}
diff --git a/frontend/src/workbench/Workbench.tsx b/frontend/src/workbench/Workbench.tsx
new file mode 100644
index 00000000000..01a4d819ac6
--- /dev/null
+++ b/frontend/src/workbench/Workbench.tsx
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+import { makeStyles } from '@material-ui/core/styles';
+import { useMachine } from '@xstate/react';
+import { HORIZONTAL, Panels, SECOND_PANEL } from 'core/panels/Panels';
+import { ExplorerWebSocketContainer } from 'explorer/ExplorerWebSocketContainer';
+import { PropertiesWebSocketContainer } from 'properties/PropertiesWebSocketContainer';
+import React, { useEffect } from 'react';
+import { OnboardArea } from 'workbench/OnboardArea';
+import { RepresentationArea } from 'workbench/RepresentationArea';
+import { Selection, WorkbenchProps } from 'workbench/Workbench.types';
+import { UpdateSelectionEvent, WorkbenchContext, WorkbenchEvent, workbenchMachine } from 'workbench/WorkbenchMachine';
+
+const useWorkbenchStyles = makeStyles((theme) => ({
+ main: {
+ display: 'grid',
+ gridTemplateRows: 'minmax(0, 1fr)',
+ gridTemplateColumns: '1fr',
+ },
+}));
+
+export const Workbench = ({
+ editingContextId,
+ initialRepresentationSelected,
+ onRepresentationSelected,
+ readOnly,
+}: WorkbenchProps) => {
+ const classes = useWorkbenchStyles();
+ const [{ context }, dispatch] = useMachine(workbenchMachine, {
+ context: {
+ displayedRepresentation: initialRepresentationSelected,
+ representations: initialRepresentationSelected ? [initialRepresentationSelected] : [],
+ },
+ });
+ const { selection, representations, displayedRepresentation } = context;
+
+ const setSelection = (selection: Selection) => {
+ const updateSelectionEvent: UpdateSelectionEvent = { type: 'UPDATE_SELECTION', selection };
+ dispatch(updateSelectionEvent);
+ };
+
+ useEffect(() => {
+ if (displayedRepresentation !== null && displayedRepresentation.id !== initialRepresentationSelected?.id) {
+ onRepresentationSelected(displayedRepresentation);
+ }
+ }, [onRepresentationSelected, initialRepresentationSelected, displayedRepresentation]);
+
+ const explorer = (
+
+ );
+
+ const properties = ;
+ let main = ;
+ if (displayedRepresentation) {
+ main = (
+
+ );
+ }
+
+ return (
+
+
+
+ }
+ initialResizablePanelSize={300}
+ />
+ );
+};
diff --git a/frontend/src/views/edit-project/EditProjectLoadedView.module.css b/frontend/src/workbench/Workbench.types.ts
similarity index 55%
rename from frontend/src/views/edit-project/EditProjectLoadedView.module.css
rename to frontend/src/workbench/Workbench.types.ts
index 7d1f8cd9524..0688940ebf3 100644
--- a/frontend/src/views/edit-project/EditProjectLoadedView.module.css
+++ b/frontend/src/workbench/Workbench.types.ts
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2019, 2020 Obeo.
+ * 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
@@ -10,15 +10,21 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
-.editProjectView {
- display: grid;
- grid-template-rows: min-content minmax(0, 1fr);
- grid-template-columns: 1fr;
- height: 100vh;
- width: 100vw;
-}
-.main {
- display: grid;
- grid-template-rows: minmax(0, 1fr);
- grid-template-columns: 1fr;
-}
+export type Selection = {
+ id: string;
+ label: string;
+ kind: string;
+};
+
+export type Representation = {
+ id: string;
+ label: string;
+ kind: string;
+};
+
+export type WorkbenchProps = {
+ editingContextId: string;
+ initialRepresentationSelected: Representation;
+ onRepresentationSelected: (representation: Representation) => void;
+ readOnly: boolean;
+};
diff --git a/frontend/src/workbench/WorkbenchMachine.ts b/frontend/src/workbench/WorkbenchMachine.ts
new file mode 100644
index 00000000000..7c5f9448aeb
--- /dev/null
+++ b/frontend/src/workbench/WorkbenchMachine.ts
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+import { Representation, Selection } from 'workbench/Workbench.types';
+import { assign, Machine } from 'xstate';
+
+export interface WorkbenchStateSchema {
+ states: { initial: {} };
+}
+
+export type SchemaValue = 'initial';
+
+export interface WorkbenchContext {
+ selection: Selection | null;
+ representations: Representation[];
+ displayedRepresentation: Representation | null;
+}
+
+export type ShowRepresentationEvent = { type: 'SHOW_REPRESENTATION'; representation: Representation };
+export type UpdateSelectionEvent = { type: 'UPDATE_SELECTION'; selection: Selection };
+export type WorkbenchEvent = UpdateSelectionEvent | ShowRepresentationEvent;
+
+export const workbenchMachine = Machine(
+ {
+ initial: 'initial',
+ context: {
+ selection: null,
+ representations: [],
+ displayedRepresentation: null,
+ },
+ states: {
+ initial: {
+ on: {
+ UPDATE_SELECTION: {
+ target: 'initial',
+ actions: 'updateSelection',
+ },
+ SHOW_REPRESENTATION: {
+ target: 'initial',
+ actions: 'showRepresentation',
+ },
+ },
+ },
+ },
+ },
+ {
+ guards: {
+ hasRemainingRepresentations: (context) => {
+ return context.representations.length >= 2;
+ },
+ },
+ actions: {
+ updateSelection: assign((context, event) => {
+ const { selection } = event as UpdateSelectionEvent;
+
+ if (selection.kind === 'Diagram' || selection.kind === 'Form') {
+ const { id, label, kind } = selection;
+ const representation: Representation = { id, label, kind };
+
+ let newRepresentations = [...context.representations];
+ const selectedRepresentation = newRepresentations.find(
+ (representation) => selection.id === representation.id
+ );
+ if (!selectedRepresentation) {
+ newRepresentations = [...newRepresentations, representation];
+ }
+
+ return { selection, displayedRepresentation: representation, representations: newRepresentations };
+ }
+
+ return { selection };
+ }),
+ showRepresentation: assign((_, event) => {
+ const { representation } = event as ShowRepresentationEvent;
+ return { displayedRepresentation: representation };
+ }),
+ },
+ }
+);