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 Sep 4, 2023
1 parent 7440877 commit 364ccb8
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Expand Up @@ -85,6 +85,7 @@ This will be fixed in the next version.
- https://github.com/eclipse-sirius/sirius-web/issues/2208[#2208] [diagram] Add Cypress tests for React Flow.
- https://github.com/eclipse-sirius/sirius-web/issues/2198[#2198] [view] Add a default Color Palette on View creation
- 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/2288[#2288] [diagram] Make IconLabel a react flow node

== v2023.8.0

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 } from '../renderer/DiagramRenderer.types';
import { MultiLabelEdgeData } from '../renderer/edge/MultiLabelEdge.types';
import { IconLabelData } from '../renderer/node/IconsLabel.types';
import { ImageNodeData } from '../renderer/node/ImageNode.types';
import { ListItemData, ListNodeData } from '../renderer/node/ListNode.types';
import { ListNodeData } from '../renderer/node/ListNode.types';
import { RectangularNodeData } from '../renderer/node/RectangularNode.types';

const defaultPosition: XYPosition = { x: 0, y: 0 };
Expand Down Expand Up @@ -104,34 +106,49 @@ 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<IconLabelData> => {
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: IconLabelData = {
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<IconLabelData> = {
id,
type: 'iconLabelNode',
data,
position: defaultPosition,
};

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 Down Expand Up @@ -161,7 +178,6 @@ const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node<ListN
},
},
faded: gqlNode.state === GQLViewModifier.Faded,
listItems,
};

if (style.withHeader && data.label) {
Expand Down Expand Up @@ -267,6 +283,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 @@ -36,6 +36,7 @@ import { useDiagramDirectEdit } from './direct-edit/useDiagramDirectEdit';
import { useDrop } from './drop/useDrop';
import { MultiLabelEdge } from './edge/MultiLabelEdge';
import { MultiLabelEdgeData } from './edge/MultiLabelEdge.types';
import { IconLabelNode } from './node/IconLabel';
import { ImageNode } from './node/ImageNode';
import { ListNode } from './node/ListNode';
import { RectangularNode } from './node/RectangularNode';
Expand All @@ -50,6 +51,7 @@ const nodeTypes: NodeTypes = {
rectangularNode: RectangularNode,
imageNode: ImageNode,
listNode: ListNode,
iconLabelNode: IconLabelNode,
};

const edgeTypes: EdgeTypes = {
Expand Down
Expand Up @@ -22,24 +22,8 @@ import { Box, Edge, Node, ReactFlowProvider, Rect, boxToRect, rectToBox } from '
import { Diagram, NodeData } from '../DiagramRenderer.types';
import { Label } from '../Label';
import { DiagramDirectEditContextProvider } from '../direct-edit/DiagramDirectEditContext';
import { ListNode } from '../node/ListNode';
import { ListNodeData } from '../node/ListNode.types';
import { RectangularNodeData } from '../node/RectangularNode.types';

const emptyNodeProps = {
selected: false,
isConnectable: true,
dragging: false,
xPos: 0,
yPos: 0,
zIndex: -1,
};

const emptyListNodeProps = {
...emptyNodeProps,
type: 'listNode',
};

const elk = new ELK();

export const prepareLayoutArea = (diagram: Diagram, renderCallback: () => void): HTMLDivElement => {
Expand Down Expand Up @@ -83,38 +67,6 @@ export const prepareLayoutArea = (diagram: Diagram, renderCallback: () => void):
});
elements.push(labelContainerElement);

// then, render list node with list item only
const listNodeElements: JSX.Element[] = [];
visibleNodes.forEach((node, index) => {
if (hiddenContainer && node.type === 'listNode') {
const data = node.data as ListNodeData;
const listNode = createElement(ListNode, {
...emptyListNodeProps,
id: node.id,
data,
key: `${node.id}-${index}`,
});

const element: JSX.Element = createElement('div', {
id: `${node.id}-${index}`,
key: node.id,
children: listNode,
});
listNodeElements.push(element);
}
});
const nodeListContainerElement: JSX.Element = createElement('div', {
id: 'hidden-nodeList-container',
key: 'hidden-nodeList-container',
style: {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'flex-start',
},
children: listNodeElements,
});
elements.push(nodeListContainerElement);

const hiddenContainerContentElements: JSX.Element = createElement(Fragment, { children: elements });

const element = (
Expand Down Expand Up @@ -228,11 +180,54 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node<NodeData>[]) =
nodeToLayout.width = defaultWidth;
nodeToLayout.height = defaultHeight;
} else if (nodeToLayout.type === 'listNode') {
const nodeList = document.getElementById(`${nodeToLayout.id}-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`)
?.children[0];
if (directChildren.length > 0) {
layoutNodes(allVisibleNodes, directChildren);
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
);

const iconLabelNodes = directChildren.filter((node) => node.type === 'iconLabelNode');
iconLabelNodes.forEach((child, index) => {
child.position = {
x: 0,
y: (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding,
};
if (index > 0) {
const previousSibling = iconLabelNodes[index - 1];
child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) };
}
});

nodeToLayout.width = getNodeOrMinWidth(nodeList?.getBoundingClientRect().width);
nodeToLayout.height = getNodeOrMinHeight(nodeList?.getBoundingClientRect().height);
const childrenFootprint = getChildrenFootprint(iconLabelNodes);
const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width;
const labelOnlyWidth =
rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding;
const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderLeftAndRight;
nodeToLayout.width = getNodeOrMinWidth(nodeWidth);
nodeToLayout.height = getNodeOrMinHeight(
(labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding + childrenFootprint.height
);

if (nodeWidth > childrenAwareNodeWidth) {
// we need to adjust the width of children
iconLabelNodes.forEach((child) => {
child.width = nodeWidth;
child.style = {
...child.style,
width: `${nodeWidth}px`,
};
});
}
} else {
nodeToLayout.width = getNodeOrMinWidth(undefined);
nodeToLayout.height = getNodeOrMinHeight(undefined);
}
} else if (nodeToLayout.type === 'iconLabelNode') {
const labelElement = document.getElementById(
`${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}`
);
nodeToLayout.width = labelElement?.getBoundingClientRect().width;
nodeToLayout.height = labelElement?.getBoundingClientRect().height;
}
nodeToLayout.style = {
...nodeToLayout.style,
Expand Down
@@ -0,0 +1,47 @@
/*******************************************************************************
* 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 { Theme, useTheme } from '@material-ui/core/styles';
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { Label } from '../Label';
import { NodePalette } from '../palette/NodePalette';
import { IconLabelData } from './IconsLabel.types';

const iconlabelStyle = (
style: React.CSSProperties,
theme: Theme,
selected: boolean,
faded: boolean
): React.CSSProperties => {
const iconLabelNodeStyle: React.CSSProperties = {
opacity: faded ? '0.4' : '',
...style,
};

if (selected) {
iconLabelNodeStyle.outline = `${theme.palette.primary.main} solid 1px`;
}

return iconLabelNodeStyle;
};

export const IconLabelNode = memo(({ data, id, selected }: NodeProps<IconLabelData>) => {
const theme = useTheme();
return (
<div style={iconlabelStyle(data.style, theme, selected, data.faded)}>
{data.label ? <Label label={data.label} faded={data.faded} transform="" /> : null}
{selected ? <NodePalette diagramElementId={id} labelId={data?.label?.id ?? null} /> : null}
</div>
);
});
@@ -0,0 +1,18 @@
/*******************************************************************************
* 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 { NodeData } from '../DiagramRenderer.types';

export interface IconLabelData extends NodeData {
style: React.CSSProperties;
}
Expand Up @@ -58,15 +58,6 @@ export const ListNode = memo(({ data, isConnectable, id, selected }: NodeProps<L
onDrop={handleOnDrop}
data-testid={`Rectangle - ${data?.label?.text}`}>
{data.label ? <Label label={data.label} faded={data.faded} transform="" /> : null}
<div>
{data.listItems.map((listItem) => {
return (
<div key={listItem.id} style={listItem.style}>
{listItem.label.text}
</div>
);
})}
</div>
{selected ? <NodePalette diagramElementId={id} labelId={data.label?.id ?? null} /> : null}
<Handle type="source" position={Position.Left} isConnectable={isConnectable} />
<Handle type="target" position={Position.Right} isConnectable={isConnectable} />
Expand Down
Expand Up @@ -15,7 +15,6 @@ import { Label, NodeData } from '../DiagramRenderer.types';

export interface ListNodeData extends NodeData {
style: React.CSSProperties;
listItems: ListItemData[];
}

export interface ListItemData {
Expand Down

0 comments on commit 364ccb8

Please sign in to comment.