Skip to content

Commit

Permalink
[2313] Support Palette on Edges on react-flow diagrams
Browse files Browse the repository at this point in the history
Bug: #2313
Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
  • Loading branch information
AxelRICHARD authored and gcoutable committed Sep 6, 2023
1 parent 65ebe0f commit 8953322
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ This will be fixed in the next version.
- https://github.com/eclipse-sirius/sirius-web/issues/2228[#2228] [domain] Creating a domain from the onboard area action now initialize its name.
- https://github.com/eclipse-sirius/sirius-web/issues/2329[#2329] [form] Change the default representation in the details view for non containment reference from a select widget to a reference widget.
- https://github.com/eclipse-sirius/sirius-web/issues/2314[#2314] [diagram] Add feedback on edge selection on react-flow diagrams
- https://github.com/eclipse-sirius/sirius-web/issues/2313[#2313] [diagram] Support edge palette on react-flow diagrams

== v2023.8.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { ListNode } from './node/ListNode';
import { RectangularNode } from './node/RectangularNode';
import { DiagramPalette } from './palette/DiagramPalette';
import { useDiagramPalette } from './palette/useDiagramPalette';
import { useEdgePalette } from './palette/useEdgePalette';
import { DiagramPanel } from './panel/DiagramPanel';
import { useReconnectEdge } from './reconnect-edge/useReconnectEdge';

Expand Down Expand Up @@ -70,6 +71,8 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
snapToGrid: false,
});
const { onDiagramBackgroundClick, hideDiagramPalette } = useDiagramPalette();
const { onEdgeClick } = useEdgePalette();

const { onConnect } = useConnector();
const { reconnectEdge } = useReconnectEdge();
const { onDrop, onDragOver } = useDrop();
Expand Down Expand Up @@ -177,6 +180,10 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
onDiagramBackgroundClick(event);
};

const handleEdgeClick = (event: React.MouseEvent<Element, MouseEvent>) => {
onEdgeClick(event);
};

const handleSnapToGrid = (snapToGrid: boolean) => setState((prevState) => ({ ...prevState, snapToGrid }));

const onKeyDown = (event: React.KeyboardEvent<Element>) => {
Expand All @@ -196,6 +203,7 @@ export const DiagramRenderer = ({ diagram, selection, setSelection }: DiagramRen
onEdgesChange={handleEdgesChange}
onEdgeUpdate={reconnectEdge}
onPaneClick={handlePaneClick}
onEdgeClick={handleEdgeClick}
onMove={() => hideDiagramPalette()}
onDrop={onDrop}
onDragOver={onDragOver}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Theme, useTheme } from '@material-ui/core/styles';
import { memo } from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath } from 'reactflow';
import { Label } from '../Label';
import { EdgePalette } from '../palette/EdgePalette';
import { MultiLabelEdgeData } from './MultiLabelEdge.types';

const multiLabelEdgeStyle = (
Expand Down Expand Up @@ -70,6 +71,7 @@ export const MultiLabelEdge = memo(
markerEnd={selected ? `${markerEnd?.slice(0, markerEnd.length - 1)}--selected)` : markerEnd}
markerStart={selected ? `${markerStart?.slice(0, markerStart.length - 1)}--selected)` : markerStart}
/>
{selected ? <EdgePalette edgeId={id} labelId={label ? label.id : null} /> : null}
<EdgeLabelRenderer>
{beginLabel && (
<Label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const DiagramPalette = ({ targetObjectId }: DiagramPaletteProps) => {
return isOpened && x && y ? (
<DiagramPalettePortal>
<div className={classes.toolbar} style={{ position: 'absolute', left: x, top: y }}>
<Palette diagramElementId={targetObjectId} onDirectEditClick={() => {}} isNodePalette={false} />
<Palette diagramElementId={targetObjectId} onDirectEditClick={() => {}} isDiagramElementPalette={false} />
</div>
</DiagramPalettePortal>
) : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*******************************************************************************
* 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 { useDiagramDirectEdit } from '../direct-edit/useDiagramDirectEdit';
import { EdgePaletteProps } from './EdgePalette.types';
import { EdgePalettePortal } from './EdgePalettePortal';
import { Palette } from './Palette';
import { useEdgePalette } from './useEdgePalette';

const useEdgePaletteStyle = 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 EdgePalette = ({ edgeId, labelId }: EdgePaletteProps) => {
const { setCurrentlyEditedLabelId } = useDiagramDirectEdit();
const { x, y, isOpened } = useEdgePalette();
const classes = useEdgePaletteStyle();

const handleDirectEditClick = () => {
if (labelId) {
setCurrentlyEditedLabelId('palette', labelId, null);
}
};

return isOpened && x && y ? (
<EdgePalettePortal>
<div className={classes.toolbar} style={{ position: 'absolute', left: x, top: y }}>
<Palette diagramElementId={edgeId} onDirectEditClick={handleDirectEditClick} isDiagramElementPalette={true} />
</div>
</EdgePalettePortal>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*******************************************************************************
* 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 EdgePaletteProps {
edgeId: string;
labelId: string | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*******************************************************************************
* 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 React, { useCallback, useState } from 'react';
import {
EdgePaletteContextProviderProps,
EdgePaletteContextProviderState,
EdgePaletteContextValue,
} from './EdgePaletteContext.types';

const defaultValue: EdgePaletteContextValue = {
x: null,
y: null,
isOpened: false,
hideEdgePalette: () => {},
showEdgePalette: () => {},
};

export const EdgePaletteContext = React.createContext<EdgePaletteContextValue>(defaultValue);

export const EdgePaletteContextProvider = ({ children }: EdgePaletteContextProviderProps) => {
const [state, setState] = useState<EdgePaletteContextProviderState>({
x: null,
y: null,
isOpened: false,
});

const showPalette = useCallback((x: number, y: number) => {
setState((prevState) => ({ ...prevState, x, y, isOpened: true }));
}, []);

const hidePalette = useCallback(() => {
setState((prevState) => ({ ...prevState, x: null, y: null, isOpened: false }));
}, []);

return (
<EdgePaletteContext.Provider
value={{
x: state.x,
y: state.y,
isOpened: state.isOpened,
showEdgePalette: showPalette,
hideEdgePalette: hidePalette,
}}>
{children}
</EdgePaletteContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* 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 EdgePaletteContextValue {
x: number | null;
y: number | null;
isOpened: boolean;
showEdgePalette: (x: number, y: number) => void;
hideEdgePalette: () => void;
}

export interface EdgePaletteContextProviderProps {
children: React.ReactNode;
}

export interface EdgePaletteContextProviderState {
x: number | null;
y: number | null;
isOpened: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*******************************************************************************
* 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';
import { createPortal } from 'react-dom';
import { ReactFlowState, useStore } from 'reactflow';

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

export const EdgePalettePortal = ({ children }: { children: ReactNode }) => {
const wrapperRef = useStore(selector);

if (!wrapperRef) {
return null;
}

return createPortal(children, wrapperRef);
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export const NodePalette = ({ diagramElementId, labelId }: NodePaletteProps) =>

return (
<NodeToolbar className={classes.toolbar} position={Position.Top}>
<Palette diagramElementId={diagramElementId} onDirectEditClick={handleDirectEditClick} isNodePalette={true} />
<Palette
diagramElementId={diagramElementId}
onDirectEditClick={handleDirectEditClick}
isDiagramElementPalette={true}
/>
</NodeToolbar>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { makeStyles } from '@material-ui/core/styles';
import TonalityIcon from '@material-ui/icons/Tonality';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
import { useCallback, useContext, useEffect, useState } from 'react';
import { useEdges, useNodes } from 'reactflow';
import { DiagramContext } from '../../contexts/DiagramContext';
import { DiagramContextValue } from '../../contexts/DiagramContext.types';
import { Tool } from '../Tool';
import { useFadeDiagramElements } from '../fade/useFadeDiagramElements';
import { useHideDiagramElements } from '../hide/useHideDiagramElements';
import { Tool } from '../Tool';
import {
ContextualPaletteStyleProps,
GQLCollapsingState,
Expand Down Expand Up @@ -157,10 +158,12 @@ const isDiagramDescription = (
representationDescription: GQLRepresentationDescription
): representationDescription is GQLDiagramDescription => representationDescription.__typename === 'DiagramDescription';

export const Palette = ({ diagramElementId, onDirectEditClick, isNodePalette }: PaletteProps) => {
export const Palette = ({ diagramElementId, onDirectEditClick, isDiagramElementPalette }: PaletteProps) => {
const [palette, setPalette] = useState<GQLPalette | undefined>(undefined);
const { fadeDiagramElements } = useFadeDiagramElements();
const { hideDiagramElements } = useHideDiagramElements();
const nodes = useNodes();
const edges = useEdges();
const { diagramId, editingContextId } = useContext<DiagramContextValue>(DiagramContext);

const toolCount =
Expand All @@ -169,7 +172,7 @@ export const Palette = ({ diagramElementId, onDirectEditClick, isNodePalette }:
palette.toolSections.filter(
(toolSection) => toolSection.tools.filter(isSingleClickOnDiagramElementTool).length > 0
).length
: 0) + (isNodePalette ? 2 : 0);
: 0) + (isDiagramElementPalette ? 2 : 0);
const classes = usePaletteStyle({ toolCount });

const [getPalette, { loading: paletteLoading, data: paletteData, error: paletteError }] = useLazyQuery<
Expand Down Expand Up @@ -235,14 +238,26 @@ export const Palette = ({ diagramElementId, onDirectEditClick, isNodePalette }:
]
);

const invokeDelete = useCallback(
(nodeIds: string[], deletionPolicy: GQLDeletionPolicy) => {
const invokeDelete = (diagramElementId: string, deletionPolicy: GQLDeletionPolicy) => {
const nodeId = nodes.find((node) => node.id === diagramElementId);
if (nodeId) {
invokeDeleteMutation([diagramElementId], [], deletionPolicy);
} else {
const edgeId = edges.find((edge) => edge.id === diagramElementId);
if (edgeId) {
invokeDeleteMutation([], [diagramElementId], deletionPolicy);
}
}
};

const invokeDeleteMutation = useCallback(
(nodeIds: string[], edgeIds: string[], deletionPolicy: GQLDeletionPolicy) => {
const input: GQLDeleteFromDiagramInput = {
id: crypto.randomUUID(),
editingContextId,
representationId: diagramId,
nodeIds,
edgeIds: [],
edgeIds,
deletionPolicy,
};
deleteElementsMutation({ variables: { input } });
Expand Down Expand Up @@ -274,10 +289,10 @@ export const Palette = ({ diagramElementId, onDirectEditClick, isNodePalette }:
onDirectEditClick();
break;
case 'semantic-delete':
invokeDelete([diagramElementId], GQLDeletionPolicy.SEMANTIC);
invokeDelete(diagramElementId, GQLDeletionPolicy.SEMANTIC);
break;
case 'graphical-delete':
invokeDelete([diagramElementId], GQLDeletionPolicy.GRAPHICAL);
invokeDelete(diagramElementId, GQLDeletionPolicy.GRAPHICAL);
break;
case 'expand':
collapseExpandElement(diagramElementId, GQLCollapsingState.EXPANDED);
Expand Down Expand Up @@ -307,7 +322,7 @@ export const Palette = ({ diagramElementId, onDirectEditClick, isNodePalette }:
{palette?.toolSections.map((toolSection) => (
<ToolSection toolSection={toolSection} onToolClick={handleToolClick} key={toolSection.id} />
))}
{isNodePalette ? (
{isDiagramElementPalette ? (
<>
<IconButton
className={classes.toolIcon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ContextualPaletteStyleProps {
export interface PaletteProps {
diagramElementId: string;
onDirectEditClick: () => void;
isNodePalette: boolean;
isDiagramElementPalette: boolean;
}

export interface GQLErrorPayload
Expand Down

0 comments on commit 8953322

Please sign in to comment.