From 364ccb8799a1b206b39a3d9e35ebec7ddf78aa58 Mon Sep 17 00:00:00 2001 From: Guillaume Coutable Date: Wed, 23 Aug 2023 17:19:08 +0200 Subject: [PATCH] [2288] Make IconLabel a react flow node Bug: https://github.com/eclipse-sirius/sirius-web/issues/2288 Signed-off-by: Guillaume Coutable --- CHANGELOG.adoc | 1 + .../src/converter/convertDiagram.ts | 72 +++++++++----- .../subscription/nodeFragment.types.ts | 4 +- .../src/renderer/DiagramRenderer.tsx | 2 + .../src/renderer/layout/layout.tsx | 99 +++++++++---------- .../src/renderer/node/IconLabel.tsx | 47 +++++++++ .../src/renderer/node/IconsLabel.types.ts | 18 ++++ .../src/renderer/node/ListNode.tsx | 9 -- .../src/renderer/node/ListNode.types.ts | 1 - 9 files changed, 163 insertions(+), 90 deletions(-) create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconLabel.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconsLabel.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index eda16b2c23..845397b1f0 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -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 diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts index 3498c52655..0bf6385ab4 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/converter/convertDiagram.ts @@ -15,6 +15,7 @@ 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, @@ -22,8 +23,9 @@ import { } 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 }; @@ -104,34 +106,49 @@ const toRectangularNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Nod return node; }; -const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node => { - const style = gqlNode.style as GQLRectangularNodeStyle; - const labelStyle = gqlNode.label.style; +const toIconLabelNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node => { + 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 = { + 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 => { + const style = gqlNode.style as GQLRectangularNodeStyle; + const labelStyle = gqlNode.label.style; const { targetObjectId, targetObjectLabel, targetObjectKind } = gqlNode; const data: ListNodeData = { @@ -161,7 +178,6 @@ const toListNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node convertNode(gqlBorderNode, gqlNode, nodes)); (gqlNode.childNodes ?? []).forEach((gqlChildNode) => convertNode(gqlChildNode, gqlNode, nodes)); + } else if (gqlNode.style.__typename === 'IconLabelNodeStyle') { + nodes.push(toIconLabelNode(gqlNode, parentNode)); } }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.types.ts index a8fa7b9dc5..4c43c111a2 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.types.ts @@ -61,4 +61,6 @@ export interface GQLImageNodeStyle extends GQLNodeStyle { imageURL: string; } -export interface GQLIconLabelNodeStyle extends GQLNodeStyle {} +export interface GQLIconLabelNodeStyle extends GQLNodeStyle { + backgroundColor: string; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx index 1bb906c154..8a6175e04c 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx @@ -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'; @@ -50,6 +51,7 @@ const nodeTypes: NodeTypes = { rectangularNode: RectangularNode, imageNode: ImageNode, listNode: ListNode, + iconLabelNode: IconLabelNode, }; const edgeTypes: EdgeTypes = { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx index b4f6ec0bb2..2b3aec566a 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/layout.tsx @@ -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 => { @@ -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 = ( @@ -228,11 +180,54 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[]) = 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, diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconLabel.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconLabel.tsx new file mode 100644 index 0000000000..421cd3ffd3 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconLabel.tsx @@ -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) => { + const theme = useTheme(); + return ( +
+ {data.label ?
+ ); +}); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconsLabel.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconsLabel.types.ts new file mode 100644 index 0000000000..5a4bdcc2b8 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/IconsLabel.types.ts @@ -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; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx index cdc3039eaa..bb89a163d3 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/node/ListNode.tsx @@ -58,15 +58,6 @@ export const ListNode = memo(({ data, isConnectable, id, selected }: NodeProps {data.label ?