Skip to content

Commit

Permalink
[1169] Add subscription on FormDescriptionEditors
Browse files Browse the repository at this point in the history
Bug: eclipse-sirius#1169
Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
  • Loading branch information
AxelRICHARD committed Apr 27, 2022
1 parent bfb87be commit a1c29a7
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ type FormDescriptionEditorRefreshedEventPayload {
type FormDescriptionEditor implements Representation {
id: ID!
metadata: RepresentationMetadata!
widgets: [FormDescriptionEditorWidget!]!
}

type FormDescriptionEditorDescription implements RepresentationDescription {
id: ID!
label: String!
}

type FormDescriptionEditorWidget {
id: ID!
kind: String!
label: String!
}
2 changes: 1 addition & 1 deletion frontend/src/diagram/DiagramWebSocketContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ export const DiagramWebSocketContainer = ({

useEffect(() => {
if (error) {
const message = 'An error has occured while trying to retrieve the diagram';
const message = 'An error has occurred while trying to retrieve the diagram';
const showToastEvent: ShowToastEvent = { type: 'SHOW_TOAST', message };
dispatch(showToastEvent);
dispatch({ type: 'HANDLE_COMPLETE' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,85 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { useSubscription } from '@apollo/client';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
import Snackbar from '@material-ui/core/Snackbar';
import { makeStyles } from '@material-ui/core/styles';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import TextFieldsIcon from '@material-ui/icons/TextFields';
import { useMachine } from '@xstate/react';
import { Kind, Widget } from 'formdescriptioneditor/FormDescriptionEditorWebSocketContainer.types';
import {
GQLFormDescriptionEditorEventInput,
GQLFormDescriptionEditorEventSubscription,
GQLFormDescriptionEditorEventVariables,
GQLFormDescriptionEditorWidget,
Kind,
} from 'formdescriptioneditor/FormDescriptionEditorWebSocketContainer.types';
import {
FormDescriptionEditorWebSocketContainerContext,
FormDescriptionEditorWebSocketContainerEvent,
formDescriptionEditorWebSocketContainerMachine,
HandleSubscriptionResultEvent,
HideToastEvent,
InitializeRepresentationEvent,
SchemaValue,
UpdateWidgetsEvent,
ShowToastEvent,
} from 'formdescriptioneditor/FormDescriptionEditorWebSocketContainerMachine';
import { WidgetEntry } from 'formdescriptioneditor/WidgetEntry';
import gql from 'graphql-tag';
import React, { useEffect } from 'react';
import { v4 as uuid } from 'uuid';
import { RepresentationComponentProps } from 'workbench/Workbench.types';

export const formDescriptionEditorEventSubscription = gql`
subscription formDescriptionEditorEvent($input: FormDescriptionEditorEventInput!) {
formDescriptionEditorEvent(input: $input) {
__typename
... on ErrorPayload {
id
message
}
... on SubscribersUpdatedEventPayload {
id
subscribers {
username
}
}
... on FormDescriptionEditorRefreshedEventPayload {
id
formDescriptionEditor {
id
metadata {
kind
label
description {
id
}
}
widgets {
id
label
kind
}
}
}
}
}
`;

const useFormDescriptionEditorStyles = makeStyles((theme) => ({
formDescriptionEditor: {
display: 'flex',
flexDirection: 'column',

width: '100%',
},
header: {
padding: '4px 8px 4px 8px',
display: 'flex',
flexDirection: 'row',
},
main: {
display: 'flex',
Expand Down Expand Up @@ -73,31 +125,91 @@ const useFormDescriptionEditorStyles = makeStyles((theme) => ({
dragOver: {
border: 'dashed 1px red',
},
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),
},
noFormDescriptionEditor: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyItems: 'center',
},
}));

const isKind = (value: string): value is Kind => {
return value === 'Textfield';
};

export const FormDescriptionEditorWebSocketContainer = ({ selection, setSelection }: RepresentationComponentProps) => {
export const FormDescriptionEditorWebSocketContainer = ({
editingContextId,
representationId,
selection,
setSelection,
}: RepresentationComponentProps) => {
const classes = useFormDescriptionEditorStyles();

const [{ value, context }, dispatch] = useMachine<
FormDescriptionEditorWebSocketContainerContext,
FormDescriptionEditorWebSocketContainerEvent
>(formDescriptionEditorWebSocketContainerMachine);
const { formDescriptionEditorWebSocketContainer } = value as SchemaValue;
const { widgets } = context;
const { toast, formDescriptionEditorWebSocketContainer } = value as SchemaValue;
const { id, formDescriptionEditor, subscribers, message } = context;

const input: GQLFormDescriptionEditorEventInput = {
id,
editingContextId,
formDescriptionEditorId: representationId,
};
const variables: GQLFormDescriptionEditorEventVariables = { input };

const { error } = useSubscription<GQLFormDescriptionEditorEventSubscription, GQLFormDescriptionEditorEventVariables>(
formDescriptionEditorEventSubscription,
{
variables,
fetchPolicy: 'no-cache',
skip: formDescriptionEditorWebSocketContainer !== 'ready',
onSubscriptionData: ({ subscriptionData }) => {
const handleDataEvent: HandleSubscriptionResultEvent = {
type: 'HANDLE_SUBSCRIPTION_RESULT',
result: subscriptionData,
};
dispatch(handleDataEvent);
},
onSubscriptionComplete: () => {
dispatch({ type: 'HANDLE_COMPLETE' });
},
}
);

useEffect(() => {
if (error) {
const message: string = 'An error has occurred while trying to retrieve the form description editor';
const showToastEvent: ShowToastEvent = { type: 'SHOW_TOAST', message };
dispatch(showToastEvent);
dispatch({ type: 'HANDLE_COMPLETE' });
}
}, [error, dispatch]);

useEffect(() => {
if (formDescriptionEditorWebSocketContainer === 'loading') {
const initializeRepresentationEvent: InitializeRepresentationEvent = {
type: 'INITIALIZE',
widgets,
};
dispatch(initializeRepresentationEvent);
}
}, [formDescriptionEditorWebSocketContainer, dispatch, widgets]);
}, [formDescriptionEditorWebSocketContainer, dispatch]);

const handleDragStart: React.DragEventHandler<HTMLDivElement> = (event) => {
event.dataTransfer.setData('text/plain', event.currentTarget.id);
Expand All @@ -116,47 +228,35 @@ export const FormDescriptionEditorWebSocketContainer = ({ selection, setSelectio
const handleDrop: React.DragEventHandler<HTMLDivElement> = (event) => {
event.currentTarget.classList.remove(classes.dragOver);

const id = event.dataTransfer.getData('text/plain');
const newWidget: Widget = {
const id: string = event.dataTransfer.getData('text/plain');
const newWidget: GQLFormDescriptionEditorWidget = {
id: uuid(),
label: id,
kind: isKind(id) ? id : 'Textfield',
};
const updatedWidgets = widgets;
const updatedWidgets: GQLFormDescriptionEditorWidget[] = formDescriptionEditor.widgets;
updatedWidgets.push(newWidget);
const updateWidgetsEvent: UpdateWidgetsEvent = {
type: 'UPDATE_WIDGETS',
widgets: updatedWidgets,
};
dispatch(updateWidgetsEvent);
};
const handleDropBefore = (event: React.DragEvent<HTMLDivElement>, widget: Widget) => {
const id = event.dataTransfer.getData('text/plain');
const newWidget: Widget = {
const handleDropBefore = (event: React.DragEvent<HTMLDivElement>, widget: GQLFormDescriptionEditorWidget) => {
const id: string = event.dataTransfer.getData('text/plain');
const newWidget: GQLFormDescriptionEditorWidget = {
id: uuid(),
label: id,
kind: isKind(id) ? id : 'Textfield',
};
const updatedWidgets = widgets;
let index = updatedWidgets.indexOf(widget);
const updatedWidgets: GQLFormDescriptionEditorWidget[] = formDescriptionEditor.widgets;
let index: number = updatedWidgets.indexOf(widget);
if (index === -1) {
updatedWidgets.push(newWidget);
} else {
updatedWidgets.splice(index, 0, newWidget);
}

const updateWidgetsEvent: UpdateWidgetsEvent = {
type: 'UPDATE_WIDGETS',
widgets: updatedWidgets,
};
dispatch(updateWidgetsEvent);
};

return (
<div className={classes.formDescriptionEditor}>
<div className={classes.header}>
<Typography>Form</Typography>
</div>
let content: JSX.Element | null = null;

if (formDescriptionEditorWebSocketContainer === 'ready' && formDescriptionEditor) {
content = (
<div className={classes.main}>
<div className={classes.widgets}>
<Typography gutterBottom>Widgets</Typography>
Expand All @@ -176,7 +276,7 @@ export const FormDescriptionEditorWebSocketContainer = ({ selection, setSelectio
<div className={classes.preview}>
<Typography>Preview</Typography>
<div className={classes.body}>
{widgets.map((widget) => (
{formDescriptionEditor.widgets.map((widget) => (
<WidgetEntry
key={widget.id}
widget={widget}
Expand All @@ -196,6 +296,53 @@ export const FormDescriptionEditorWebSocketContainer = ({ selection, setSelectio
</div>
</div>
</div>
);
}

if (formDescriptionEditorWebSocketContainer === 'complete') {
content = (
<div className={classes.main + ' ' + classes.noFormDescriptionEditor}>
<Typography variant="h5" align="center" data-testid="FormDescriptionEditor-complete-message">
The form description editor does not exist
</Typography>
</div>
);
}

return (
<div className={classes.formDescriptionEditor}>
<div className={classes.header}>
<Typography>Form</Typography>
<div className={classes.subscribers}>
{subscribers.map((subscriber) => (
<Tooltip title={subscriber.username} arrow key={subscriber.username}>
<Avatar classes={{ root: classes.avatar }}>{subscriber.username.substring(0, 1).toUpperCase()}</Avatar>
</Tooltip>
))}
</div>
</div>
{content}
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
open={toast === 'visible'}
autoHideDuration={3000}
onClose={() => dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)}
message={message}
action={
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={() => dispatch({ type: 'HIDE_TOAST' } as HideToastEvent)}
>
<CloseIcon fontSize="small" />
</IconButton>
}
data-testid="error"
/>
</div>
);
};
Loading

0 comments on commit a1c29a7

Please sign in to comment.