Skip to content

Commit

Permalink
[2094] Add the support for a diagram palette
Browse files Browse the repository at this point in the history
Bug: #2094
Signed-off-by: Guillaume Coutable <guillaume.coutable@obeo.fr>
  • Loading branch information
gcoutable committed Jul 3, 2023
1 parent 4d9c791 commit 7814cfe
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [ADR-103] Improves the existing feedback messages capability
- [ADR-104] Add support for Help Expressions in Form widgets
- [ADR-105] Add custom widget to edit EMF references
- [ADR-106] Add the support for the palette with react flow

=== Breaking changes

Expand Down Expand Up @@ -53,6 +54,7 @@ image:doc/screenshots/ShowIconOptionSelectWidget.jpg[Icons on select widget opti
- https://github.com/eclipse-sirius/sirius-components/issues/2056[#2056] [form] Add the possibility to control read-only mode for widgets with an expression.
- https://github.com/eclipse-sirius/sirius-components/issues/2077[#2077] [form] Add the ability to define a border style for groups and flexbox containers.
- https://github.com/eclipse-sirius/sirius-components/issues/2080[#2080] [tree] Add an inital label value when editing tree items label.
- https://github.com/eclipse-sirius/sirius-components/issues/2094[#2094] [diagram] Add the support for the palette on the diagram background

=== Improvements

Expand Down
25 changes: 25 additions & 0 deletions doc/adrs/106_add_support_for_palette_with_reactflow.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
= [ADR-106] Add the support for the palette with reactflow

== Context

We have chosen to use reactflow to do the layout of diagram.
In the early use, the layout will be done in the frontend.
We need to support features like a palette.
The palette will be displayed on top of the selected node and at the position of the click on the diagram pane.

== Decision

We will rely on the https://reactflow.dev/docs/api/nodes/node-toolbar/[reactflow NodeToolbar] for node and our implementation for the diagram palette.

In the first version, when a node is selected, its palette will be displayed.
Once opened, the diagram palette will be displayed until the selection is changed.
The palette will be closed when the diagram pane is dragged.

== Status

Accepted.

== Consequences

Once opened the palette will almost always be opened.
We may add a way to close the palette manually later.
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const convertDiagram = (gqlDiagram: GQLDiagram): Diagram => {
id: gqlDiagram.id,
label: gqlDiagram.metadata.label,
kind: gqlDiagram.metadata.kind,
targetObjectId: gqlDiagram.targetObjectId,
},
nodes,
edges,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,34 @@ import {
useReactFlow,
useStoreApi,
} from 'reactflow';
import { DiagramRendererProps, DiagramRendererState, NodeData } from './DiagramRenderer.types';
import { DiagramPaletteState, DiagramRendererProps, DiagramRendererState, NodeData } from './DiagramRenderer.types';
import { ImageNode } from './ImageNode';
import { ListNode } from './ListNode';
import { RectangularNode } from './RectangularNode';

import 'reactflow/dist/style.css';
import { DiagramPanel } from './DiagramPanel';
import { DiagramPalette } from './palette/DiagramPalette';

const nodeTypes: NodeTypes = {
rectangularNode: RectangularNode,
imageNode: ImageNode,
listNode: ListNode,
};

const initialPaletteState = {
opened: false,
x: 0,
y: 0,
};

const isSelectChange = (change: NodeChange): change is NodeSelectionChange => change.type === 'select';
const computePalettePosition = (event: MouseEvent | React.MouseEvent, bounds?: DOMRect) => {
return {
x: event.clientX - (bounds?.left ?? 0),
y: event.clientY - (bounds?.top ?? 0),
};
};

export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRendererProps) => {
const reactFlow = useReactFlow();
Expand All @@ -52,12 +65,14 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
fullscreen: false,
snapToGrid: false,
});
const [diagramPaletteState, setDiagramPaletteState] = useState<DiagramPaletteState>(initialPaletteState);
const [nodes, setNodes, onNodesChange] = useNodesState(diagram.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(diagram.edges);

useEffect(() => {
setNodes(diagram.nodes);
setEdges(diagram.edges);
setDiagramPaletteState(initialPaletteState);
}, [diagram]);

useEffect(() => {
Expand Down Expand Up @@ -95,13 +110,21 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
if (selectionEntries.length > 0 && shouldUpdateSelection) {
setSelection({ entries: selectionEntries });
}

if (selectionEntries.length > 0) {
setDiagramPaletteState(initialPaletteState);
}
};

const handleEdgesChange: OnEdgesChange = (changes: EdgeChange[]) => {
onEdgesChange(changes);
};

const handlePaneClick = () => {
const handlePaneClick = (event: React.MouseEvent<Element, MouseEvent>) => {
const { domNode } = store.getState();
const element = domNode?.getBoundingClientRect();
const palettePosition = computePalettePosition(event, element);

const selection: Selection = {
entries: [
{
Expand All @@ -111,7 +134,18 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
},
],
};

setSelection(selection);
setDiagramPaletteState((prevState) => ({
...prevState,
opened: true,
x: palettePosition.x,
y: palettePosition.y,
}));
};

const handlePaneMoveStart = () => {
setDiagramPaletteState(initialPaletteState);
};

useEffect(() => {
Expand Down Expand Up @@ -146,6 +180,7 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
edges={edges}
onEdgesChange={handleEdgesChange}
onPaneClick={handlePaneClick}
onMoveStart={handlePaneMoveStart}
maxZoom={40}
minZoom={0.1}
snapToGrid={state.snapToGrid}
Expand Down Expand Up @@ -174,6 +209,9 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
snapToGrid={state.snapToGrid}
onSnapToGrid={handleSnapToGrid}
/>
{diagramPaletteState.opened ? (
<DiagramPalette targetObjectId={diagram.metadata.id} x={diagramPaletteState.x} y={diagramPaletteState.y} />
) : null}
</ReactFlow>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface DiagramRendererState {
snapToGrid: boolean;
}

export interface DiagramPaletteState {
opened: boolean;
x: number;
y: number;
}

export interface Diagram {
metadata: DiagramMetadata;
nodes: Node[];
Expand All @@ -35,6 +41,7 @@ export interface DiagramMetadata {
id: string;
kind: string;
label: string;
targetObjectId: string;
}

export interface NodeData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ServerContext } from '@eclipse-sirius/sirius-components-core';
import { memo, useContext } from 'react';
import { Handle, NodeProps, NodeResizer, Position } from 'reactflow';
import { ImageNodeData } from './ImageNode.types';
import { Palette } from './Palette';
import { NodePalette } from './palette/NodePalette';

const imageNodeStyle = (style: React.CSSProperties, selected: boolean): React.CSSProperties => {
const imageNodeStyle: React.CSSProperties = { width: '100%', height: '100%', ...style };
Expand All @@ -34,7 +34,7 @@ export const ImageNode = memo(({ data, isConnectable, id, selected }: NodeProps<
<>
<NodeResizer color="var(--blue-lagoon)" isVisible={selected} />
<img src={httpOrigin + data.imageURL} style={imageNodeStyle(data.style, selected)} />
{selected ? <Palette diagramElementId={id} /> : null}
{selected ? <NodePalette diagramElementId={id} /> : null}
<Handle type="source" position={Position.Left} isConnectable={isConnectable} />
<Handle type="target" position={Position.Right} isConnectable={isConnectable} />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { memo } from 'react';
import { Handle, NodeProps, NodeResizer, Position } from 'reactflow';
import { ListNodeData } from './ListNode.types';
import { Palette } from './Palette';
import { NodePalette } from './palette/NodePalette';

const listNodeStyle = (style: React.CSSProperties, selected: boolean): React.CSSProperties => {
const listNodeStyle: React.CSSProperties = {
Expand Down Expand Up @@ -71,7 +71,7 @@ export const ListNode = memo(({ data, isConnectable, id, selected }: NodeProps<L
);
})}
</div>
{selected ? <Palette diagramElementId={id} /> : null}
{selected ? <NodePalette diagramElementId={id} /> : null}
<Handle type="source" position={Position.Left} isConnectable={isConnectable} />
<Handle type="target" position={Position.Right} isConnectable={isConnectable} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { memo } from 'react';
import { Handle, NodeProps, NodeResizer, Position } from 'reactflow';
import { RectangularNodeData } from './RectangularNode.types';

import { Palette } from './Palette';
import { NodePalette } from './palette/NodePalette';

const rectangularNodeStyle = (style: React.CSSProperties, selected: boolean): React.CSSProperties => {
const rectangularNodeStyle: React.CSSProperties = {
Expand Down Expand Up @@ -45,7 +45,7 @@ export const RectangularNode = memo(({ data, isConnectable, id, selected }: Node
<NodeResizer color="var(--blue-lagoon)" isVisible={selected} />
<div style={rectangularNodeStyle(data.style, selected)}>
<div style={labelStyle(data.label.style)}>{data.label.text}</div>
{selected ? <Palette diagramElementId={id} /> : null}
{selected ? <NodePalette diagramElementId={id} /> : null}
<Handle type="source" position={Position.Left} isConnectable={isConnectable} />
<Handle type="target" position={Position.Right} isConnectable={isConnectable} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Obeo - initial API and implementation
*******************************************************************************/

import { GQLTool } from './Palette.types';
import { GQLTool } from './palette/Palette.types';

export interface ToolProps {
tool: GQLTool;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*******************************************************************************
* Copyright (c) 2023 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 { DiagramPaletteProps } from './DiagramPalette.types';
import { DiagramPalettePortal } from './DiagramPalettePortal';
import { Palette } from './Palette';

const useDiagramPaletteStyle = makeStyles((theme) => ({
toolbar: {
background: theme.palette.background.paper,
border: '1px solid #d1dadb',
boxShadow: '0px 2px 5px #002b3c40',
borderRadius: '2px',
zIndex: 2,
position: 'fixed',
display: 'flex',
alignItems: 'center',
},
}));

export const DiagramPalette = ({ targetObjectId, x, y }: DiagramPaletteProps) => {
const classes = useDiagramPaletteStyle();
return (
<DiagramPalettePortal>
<div className={classes.toolbar} style={{ position: 'absolute', left: x, top: y }}>
<Palette diagramElementId={targetObjectId} />
</div>
</DiagramPalettePortal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* Copyright (c) 2023 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 { ReactNode } from 'react';

export interface DiagramPaletteProps {
targetObjectId: string;

x: number;
y: number;
}

export interface DiagramPalettePortalProps {
children: ReactNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
* Copyright (c) 2023 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 { createPortal } from 'react-dom';
import { ReactFlowState, useStore } from 'reactflow';
import { DiagramPalettePortalProps } from './DiagramPalette.types';

const selector = (state: ReactFlowState) => state.domNode?.querySelector('.react-flow__renderer');

export const DiagramPalettePortal = ({ children }: DiagramPalettePortalProps) => {
const wrapperRef = useStore(selector);

if (!wrapperRef) {
return null;
}

return createPortal(children, wrapperRef);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2023 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 { NodeToolbar, Position } from 'reactflow';
import { NodePaletteProps } from './NodePalette.types';
import { Palette } from './Palette';

const useNodePaletteStyle = makeStyles((theme) => ({
toolbar: {
background: theme.palette.background.paper,
border: '1px solid #d1dadb',
boxShadow: '0px 2px 5px #002b3c40',
borderRadius: '2px',
zIndex: 2,
position: 'fixed',
display: 'flex',
alignItems: 'center',
},
}));

export const NodePalette = ({ diagramElementId }: NodePaletteProps) => {
const classes = useNodePaletteStyle();
return (
<NodeToolbar className={classes.toolbar} position={Position.Top}>
<Palette diagramElementId={diagramElementId} />
</NodeToolbar>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 2023 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 interface NodePaletteProps {
diagramElementId: string;
}
Loading

0 comments on commit 7814cfe

Please sign in to comment.