Skip to content

Commit

Permalink
[2288] Make IconLabel a react flow node
Browse files Browse the repository at this point in the history
Bug: #2288
Signed-off-by: Guillaume Coutable <guillaume.coutable@obeo.fr>
  • Loading branch information
gcoutable committed Oct 10, 2023
1 parent 17b8438 commit 52bbc9f
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 90 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Expand Up @@ -43,6 +43,7 @@ In order to provide a first version of those capabilities, some additional coupl
This coupling will be removed in the near future as a consequence, the dependencies of this code will change and some APIs of the reference widget will be broken in the near future.
- https://github.com/eclipse-sirius/sirius-web/issues/2380[#2380] [diagram] Add typing to reactflow hooks
- https://github.com/eclipse-sirius/sirius-web/issues/2430[#2430] [diagram] Improve fit to screen feature to zoom on selected nodes
- https://github.com/eclipse-sirius/sirius-web/issues/2288[#2288] [diagram] Make IconLabel a react flow node


== v2023.10.0
Expand Down Expand Up @@ -189,7 +190,6 @@ To illustrate this new feature, we contribute a new tool on the _Papaya Diagram_
These color palettes are accessible in studio projects but are not visible in the Explorer view.
- https://github.com/eclipse-sirius/sirius-web/issues/2371[#2371] [view] Add new validation rules about colors for View diagrams & forms.


== v2023.8.0

=== Shapes
Expand Down
Expand Up @@ -37,9 +37,11 @@ describe('/projects/:projectId/edit - Diagram', () => {

cy.getByTestId('rf__wrapper').should('exist');
cy.get('.react-flow__edgelabel-renderer').children().should('have.length', 7);
cy.get('.react-flow__nodes').children().should('have.length', 14);
cy.get('.react-flow__nodes').children().should('have.length', 18);
cy.get('.react-flow__node-rectangularNode').should('have.length', 2);
cy.get('.react-flow__node-imageNode').should('have.length', 10);
cy.get('.react-flow__node-listNode').should('have.length', 2);
cy.get('.react-flow__node-iconLabelNode').should('have.length', 4);
});

it('can share the representation', () => {
Expand Down
Expand Up @@ -15,15 +15,17 @@ import { Edge, Node, XYPosition } from 'reactflow';
import { GQLDiagram } from '../graphql/subscription/diagramFragment.types';
import { GQLLabel, GQLLabelStyle } from '../graphql/subscription/labelFragment.types';
import {
GQLIconLabelNodeStyle,
GQLImageNodeStyle,
GQLNode,
GQLRectangularNodeStyle,
GQLViewModifier,
} from '../graphql/subscription/nodeFragment.types';
import { Diagram, Label, NodeData } from '../renderer/DiagramRenderer.types';
import { MultiLabelEdgeData } from '../renderer/edge/MultiLabelEdge.types';
import { IconLabelNodeData } from '../renderer/node/IconsLabelNode.types';
import { ImageNodeData } from '../renderer/node/ImageNode.types';
import { ListItemData, ListNodeData } from '../renderer/node/ListNode.types';
import { ListNodeData } from '../renderer/node/ListNode.types';
import { DiagramNodeType } from '../renderer/node/NodeTypes.types';
import { RectangularNodeData } from '../renderer/node/RectangularNode.types';

Expand Down Expand Up @@ -105,34 +107,50 @@ const toRectangularNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Nod
return node;
};

const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<ListNodeData> => {
const style = gqlNode.style as GQLRectangularNodeStyle;
const labelStyle = gqlNode.label.style;
const toIconLabelNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<IconLabelNodeData> => {
const { targetObjectId, targetObjectLabel, targetObjectKind } = gqlNode;
const style = gqlNode.style as GQLIconLabelNodeStyle;
const { id, label } = gqlNode;
const labelStyle = label.style;

const listItems: ListItemData[] = (gqlNode.childNodes ?? []).map((gqlChildNode) => {
const { id, label } = gqlChildNode;
return {
id,
label: {
id: label.id,
text: label.text,
iconURL: null,
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: '8px',
padding: '4px 8px',
},
},
const data: IconLabelNodeData = {
targetObjectId,
targetObjectLabel,
targetObjectKind,
style: {
textAlign: 'left',
backgroundColor: style.backgroundColor,
},
label: {
id: label.id,
text: label.text,
style: {
textAlign: 'left',
...convertLabelStyle(label.style),
...convertLabelStyle(labelStyle),
},
hidden: gqlChildNode.state === GQLViewModifier.Hidden,
};
});
iconURL: labelStyle.iconURL,
},
faded: gqlNode.state === GQLViewModifier.Faded,
};

const node: Node<IconLabelNodeData> = {
id,
type: 'iconLabelNode',
data,
position: defaultPosition,
hidden: gqlNode.state === GQLViewModifier.Hidden,
};

if (gqlParentNode) {
node.parentNode = gqlParentNode.id;
node.extent = 'parent';
}

return node;
};

const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<ListNodeData> => {
const style = gqlNode.style as GQLRectangularNodeStyle;
const labelStyle = gqlNode.label.style;

const { targetObjectId, targetObjectLabel, targetObjectKind } = gqlNode;
const data: ListNodeData = {
Expand All @@ -155,14 +173,12 @@ const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<ListN
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
padding: '8px 16px',
textAlign: 'center',
...convertLabelStyle(labelStyle),
},
},
faded: gqlNode.state === GQLViewModifier.Faded,
listItems,
};

if (style.withHeader && data.label) {
Expand Down Expand Up @@ -235,7 +251,6 @@ const toImageNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<Imag
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
padding: '8px 16px',
textAlign: 'center',
...convertLabelStyle(labelStyle),
Expand Down Expand Up @@ -283,6 +298,8 @@ const convertNode = (gqlNode: GQLNode, parentNode: GQLNode | null, nodes: Node[]

(gqlNode.borderNodes ?? []).forEach((gqlBorderNode) => convertNode(gqlBorderNode, gqlNode, nodes));
(gqlNode.childNodes ?? []).forEach((gqlChildNode) => convertNode(gqlChildNode, gqlNode, nodes));
} else if (gqlNode.style.__typename === 'IconLabelNodeStyle') {
nodes.push(toIconLabelNode(gqlNode, parentNode));
}
};

Expand Down
Expand Up @@ -61,4 +61,6 @@ export interface GQLImageNodeStyle extends GQLNodeStyle {
imageURL: string;
}

export interface GQLIconLabelNodeStyle extends GQLNodeStyle {}
export interface GQLIconLabelNodeStyle extends GQLNodeStyle {
backgroundColor: string;
}
Expand Up @@ -39,16 +39,16 @@ import { useDiagramDirectEdit } from './direct-edit/useDiagramDirectEdit';
import { useDrop } from './drop/useDrop';
import { edgeTypes } from './edge/EdgeTypes';
import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types';
import { useLayout } from './layout/useLayout';
import { nodeTypes } from './node/NodeTypes';
import { DiagramNodeType } from './node/NodeTypes.types';
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';

import 'reactflow/dist/style.css';
import { useLayout } from './layout/useLayout';
import { DiagramNodeType } from './node/NodeTypes.types';

const isNodeSelectChange = (change: NodeChange): change is NodeSelectionChange => change.type === 'select';
const isEdgeSelectChange = (change: EdgeChange): change is EdgeSelectionChange => change.type === 'select';
Expand Down
Expand Up @@ -22,9 +22,10 @@ const labelStyle = (
theme: Theme,
style: React.CSSProperties,
faded: Boolean,
transform: string
transform: string,
hasIcon: boolean
): React.CSSProperties => {
return {
const labelStyle: React.CSSProperties = {
transform,
opacity: faded ? '0.4' : '',
pointerEvents: 'all',
Expand All @@ -35,6 +36,12 @@ const labelStyle = (
...style,
color: style.color ? getCSSColor(String(style.color), theme) : undefined,
};

if (hasIcon) {
labelStyle.gap = '8px';
}

return labelStyle;
};

export const Label = memo(({ diagramElementId, label, faded, transform }: LabelProps) => {
Expand All @@ -59,12 +66,13 @@ export const Label = memo(({ diagramElementId, label, faded, transform }: LabelP
<DiagramDirectEditInput editingKey={editingKey} onClose={handleClose} labelId={label.id} transform={transform} />
);
}

return (
<div
data-id={label.id}
data-testid={`Label - ${label.text}`}
onDoubleClick={handleDoubleClick}
style={labelStyle(theme, label.style, faded, transform)}
style={labelStyle(theme, label.style, faded, transform, !!label.iconURL)}
className="nopan">
{label.iconURL ? <img src={httpOrigin + label.iconURL} /> : ''}
{label.text}
Expand Down
@@ -0,0 +1,40 @@
/*******************************************************************************
* 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 { Node } from 'reactflow';
import { Diagram, NodeData } from '../DiagramRenderer.types';
import { IconLabelNodeData } from '../node/IconsLabelNode.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { ILayoutEngine, INodeLayoutHandler } from './LayoutEngine.types';

export class IconLabelNodeLayoutHandler implements INodeLayoutHandler<IconLabelNodeData> {
canHandle(node: Node<NodeData, DiagramNodeType>) {
return node.type === 'iconLabelNode';
}
handle(
_layoutEngine: ILayoutEngine,
_previousDiagram: Diagram | null,
node: Node<IconLabelNodeData>,
visibleNodes: Node<NodeData, DiagramNodeType>[],
_directChildren: Node<NodeData, DiagramNodeType>[]
) {
const nodeIndex = this.findNodeIndex(visibleNodes, node.id);
const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`);

node.width = labelElement?.getBoundingClientRect().width;
node.height = labelElement?.getBoundingClientRect().height;
}

private findNodeIndex(nodes: Node<NodeData>[], nodeId: string): number {
return nodes.findIndex((node) => node.id === nodeId);
}
}
Expand Up @@ -14,6 +14,7 @@
import { Node } from 'reactflow';
import { Diagram, NodeData } from '../DiagramRenderer.types';
import { DiagramNodeType } from '../node/NodeTypes.types';
import { IconLabelNodeLayoutHandler } from './IconLabelNodeLayoutHandler';
import { ImageNodeLayoutHandler } from './ImageNodeLayoutHandler';
import { ILayoutEngine, INodeLayoutHandler } from './LayoutEngine.types';
import { ListNodeLayoutHandler } from './ListNodeLayoutHandler';
Expand All @@ -24,6 +25,7 @@ export class LayoutEngine implements ILayoutEngine {
new RectangleNodeLayoutHandler(),
new ListNodeLayoutHandler(),
new ImageNodeLayoutHandler(),
new IconLabelNodeLayoutHandler(),
];

public layoutNodes(
Expand Down

0 comments on commit 52bbc9f

Please sign in to comment.