diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e019e7889c..69fa2eca2c 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -122,6 +122,7 @@ To illustrate this new feature, we contribute a new tool on the _Papaya Diagram_ - https://github.com/eclipse-sirius/sirius-web/issues/2365[#2365] [diagram] Support key down and F2 to enable direct edit on edges on react-flow diagrams. - https://github.com/eclipse-sirius/sirius-web/issues/2358[#2358] [form] Adds diagnostic messages to the reference widget. - https://github.com/eclipse-sirius/sirius-web/issues/2288[#2288] [diagram] Make IconLabel a react flow node +- https://github.com/eclipse-sirius/sirius-web/issues/2289[#2289] [diagram] Use the layout strategy to layout icon label nodes == v2023.8.0 diff --git a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls index 5a4caa3475..761b719da5 100644 --- a/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls +++ b/packages/diagrams/backend/sirius-components-collaborative-diagrams/src/main/resources/schema/diagram.graphqls @@ -44,10 +44,21 @@ type Node { position: Position! state: ViewModifier! style: INodeStyle! + childrenLayoutStrategy: ILayoutStrategy borderNodes: [Node!]! childNodes: [Node!]! } +type FreeFormLayoutStrategy { + kind: String! +} + +type ListLayoutStrategy { + kind: String! +} + +union ILayoutStrategy = FreeFormLayoutStrategy | ListLayoutStrategy + type Label { id: ID! text: String! 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 fce43232ae..3670a36a14 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 @@ -278,9 +278,7 @@ const toImageNode = (gqlNode: GQLNode, gqlParentNode: GQLNode | null): Node { if (gqlNode.style.__typename === 'RectangularNodeStyle') { - const isList = - (gqlNode.childNodes ?? []).filter((gqlChildNode) => gqlChildNode.style.__typename === 'IconLabelNodeStyle') - .length > 0; + const isList = gqlNode.childrenLayoutStrategy?.kind === 'List'; if (!isList) { nodes.push(toRectangularNode(gqlNode, parentNode)); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.ts index 158c0c9e36..9239a1bf84 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/graphql/subscription/nodeFragment.ts @@ -44,6 +44,15 @@ fragment nodeFragment on Node { backgroundColor } } + childrenLayoutStrategy { + __typename + ... on ListLayoutStrategy { + kind + } + ... on FreeFormLayoutStrategy { + kind + } + } userResizable } `; 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 4c43c111a2..4965046721 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 @@ -22,12 +22,26 @@ export interface GQLNode { state: GQLViewModifier; label: GQLLabel; style: GQLNodeStyle; + childrenLayoutStrategy?: ILayoutStrategy; borderNodes: GQLNode[] | undefined; childNodes: GQLNode[] | undefined; position: GQLPosition; size: GQLSize; } +export interface ILayoutStrategy { + __typename: string; + kind: string; +} + +export interface ListLayoutStrategy extends ILayoutStrategy { + kind: 'List'; +} + +export interface FreeFormLayoutStrategy extends ILayoutStrategy { + kind: 'FreeForm'; +} + export enum GQLViewModifier { Normal = 'Normal', Faded = 'Faded', 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 ed5d7e107e..64ee50f84f 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 @@ -27,6 +27,15 @@ import { ImageNodeData } from '../node/ImageNode.types'; import { ListNodeData } from '../node/ListNode.types'; import { RectangularNodeData } from '../node/RectangularNode.types'; +const isListNode = (node: Node): node is Node => node.type === 'listNode'; +const isRectangularNode = (node: Node): node is Node => node.type === 'rectangularNode'; +const isImageNode = (node: Node): node is Node => node.type === 'imageNode'; +const isIconLabelNode = (node: Node): node is Node => node.type === 'iconLabelNode'; +const getBorderWidth = (style: React.CSSProperties): number => { + const borderWidth = Number(style.borderWidth); + return !Number.isNaN(borderWidth) ? borderWidth : 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style +}; + const elk = new ELK(); export const prepareLayoutArea = (diagram: Diagram, renderCallback: () => void, httpOrigin: string): HTMLDivElement => { @@ -120,16 +129,11 @@ export const layout = (diagram: Diagram): Diagram => { const findNodeIndex = (nodes: Node[], refNodeId: string): number => nodes.findIndex((node) => node.id === refNodeId); -const isListNode = (node: Node): node is Node => node.type === 'listNode'; -const isRectangularNode = (node: Node): node is Node => node.type === 'rectangularNode'; -const isImageNode = (node: Node): node is Node => node.type === 'imageNode'; -const isIconLabelNode = (node: Node): node is Node => node.type === 'iconLabelNode'; - const layoutDiagram = (diagram: Diagram) => { const allVisibleNodes = diagram.nodes.filter((node) => !node.hidden); const nodesToLayout = allVisibleNodes.filter((node) => !node.parentNode); - layoutNodes(allVisibleNodes, nodesToLayout); + layoutNodes(allVisibleNodes, nodesToLayout, null); // Update position of root nodes nodesToLayout.forEach((rootNode, index) => { rootNode.position = { x: 0, y: 0 }; @@ -140,13 +144,13 @@ const layoutDiagram = (diagram: Diagram) => { }); }; -const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[]) => { +const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[], forceWidth: number | null) => { nodesToLayout.forEach((nodeToLayout) => { const directChildren = allVisibleNodes.filter((node) => node.parentNode === nodeToLayout.id); if (isRectangularNode(nodeToLayout)) { - const borderWidth = (nodeToLayout.data.style.borderWidth as number) ?? 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style + const borderWidth = getBorderWidth(nodeToLayout.data.style); if (directChildren.length > 0) { - layoutNodes(allVisibleNodes, directChildren); + layoutNodes(allVisibleNodes, directChildren, null); const labelElement = document.getElementById( `${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}` @@ -155,8 +159,12 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[]) = // Update children position to be under the label and at the right padding. directChildren.forEach((child, index) => { child.position = { - x: rectangularNodePadding, - y: rectangularNodePadding + (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding, + x: borderWidth + rectangularNodePadding, + y: + borderWidth + + rectangularNodePadding + + (labelElement?.getBoundingClientRect().height ?? 0) + + rectangularNodePadding, }; const previousSibling = directChildren[index - 1]; if (previousSibling) { @@ -167,19 +175,18 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[]) = // Update node to layout size // WARN: We suppose label are always on top of children (that wrong) const childrenFootprint = getChildrenFootprint(directChildren); - const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width + rectangularNodePadding; - const labelOnlyWidth = - rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding; - const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderWidth * 2; - nodeToLayout.width = getNodeOrMinWidth(nodeWidth); - nodeToLayout.height = getNodeOrMinHeight( + const labelOnlyWidth = labelElement?.getBoundingClientRect().width ?? 0; + const nodeWidth = + Math.max(childrenFootprint.width, labelOnlyWidth) + rectangularNodePadding * 2 + borderWidth * 2; + const nodeHeight = rectangularNodePadding + - (labelElement?.getBoundingClientRect().height ?? 0) + - rectangularNodePadding + - childrenFootprint.height + - rectangularNodePadding + - borderWidth * 2 - ); + (labelElement?.getBoundingClientRect().height ?? 0) + + rectangularNodePadding + + childrenFootprint.height + + rectangularNodePadding + + borderWidth * 2; + nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth); + nodeToLayout.height = getNodeOrMinHeight(nodeHeight); } else { const labelElement = document.getElementById( `${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}` @@ -190,68 +197,67 @@ const layoutNodes = (allVisibleNodes: Node[], nodesToLayout: Node[]) = (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding + borderWidth * 2; + const labelHeight = rectangularNodePadding + (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding; - const nodeWidth = getNodeOrMinWidth(labelWidth); - const nodeHeight = getNodeOrMinHeight(labelHeight); - nodeToLayout.width = nodeWidth; - nodeToLayout.height = nodeHeight; + nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(labelWidth); + nodeToLayout.height = getNodeOrMinHeight(labelHeight); } } else if (isImageNode(nodeToLayout)) { - nodeToLayout.width = defaultWidth; + nodeToLayout.width = forceWidth ?? defaultWidth; nodeToLayout.height = defaultHeight; } else if (isListNode(nodeToLayout)) { + const borderWidth = getBorderWidth(nodeToLayout.data.style); if (directChildren.length > 0) { - layoutNodes(allVisibleNodes, directChildren); + layoutNodes(allVisibleNodes, directChildren, forceWidth); const labelElement = document.getElementById( `${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}` ); - const iconLabelNodes = directChildren.filter(isIconLabelNode); - iconLabelNodes.forEach((child, index) => { + if (!forceWidth) { + const widerWidth = directChildren.reduce( + (widerWidth, child) => Math.max(child.width ?? 0, widerWidth), + labelElement?.getBoundingClientRect().width ?? 0 + ); + + layoutNodes(allVisibleNodes, directChildren, widerWidth); + } + + directChildren.forEach((child, index) => { child.position = { - x: 0, - y: (labelElement?.getBoundingClientRect().height ?? 0) + rectangularNodePadding, + x: borderWidth, + y: borderWidth + (labelElement?.getBoundingClientRect().height ?? 0), }; - const previousSibling = iconLabelNodes[index - 1]; + const previousSibling = directChildren[index - 1]; if (previousSibling) { child.position = { ...child.position, y: previousSibling.position.y + (previousSibling.height ?? 0) }; } }); - const childrenFootprint = getChildrenFootprint(iconLabelNodes); - const childrenAwareNodeWidth = childrenFootprint.x + childrenFootprint.width + rectangularNodePadding; - const labelOnlyWidth = - rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding; - const borderWidth = (nodeToLayout.data.style.borderWidth as number) ?? 0; // WARN: Works only if the value is in pixel. It may need to pre-compute the layout to have the pixel value of the computed style - const nodeWidth = Math.max(childrenAwareNodeWidth, labelOnlyWidth) + borderWidth * 2; - nodeToLayout.width = getNodeOrMinWidth(nodeWidth); - nodeToLayout.height = getNodeOrMinHeight( - (labelElement?.getBoundingClientRect().height ?? 0) + - rectangularNodePadding + - childrenFootprint.height + - borderWidth * 2 + const childrenFootprint = getChildrenFootprint(directChildren); + const labelOnlyWidth = labelElement?.getBoundingClientRect().width ?? 0; + const nodeWidth = Math.max(childrenFootprint.width, labelOnlyWidth) + borderWidth * 2; + nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth); + nodeToLayout.height = + getNodeOrMinHeight((labelElement?.getBoundingClientRect().height ?? 0) + childrenFootprint.height) + + borderWidth * 2; + } else { + const labelElement = document.getElementById( + `${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}` ); - 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); + const nodeWidth = (labelElement?.getBoundingClientRect().width ?? 0) + borderWidth * 2; + const nodeHeight = (labelElement?.getBoundingClientRect().height ?? 0) + borderWidth * 2; + nodeToLayout.width = forceWidth ?? getNodeOrMinWidth(nodeWidth); + nodeToLayout.height = getNodeOrMinHeight(nodeHeight); } } else if (isIconLabelNode(nodeToLayout)) { const labelElement = document.getElementById( `${nodeToLayout.id}-label-${findNodeIndex(allVisibleNodes, nodeToLayout.id)}` ); - nodeToLayout.width = labelElement?.getBoundingClientRect().width; + nodeToLayout.width = + forceWidth ?? + rectangularNodePadding + (labelElement?.getBoundingClientRect().width ?? 0) + rectangularNodePadding; nodeToLayout.height = labelElement?.getBoundingClientRect().height; } nodeToLayout.style = { @@ -297,7 +303,10 @@ const getBoundsOfBoxes = (box1: Box, box2: Box): Box => { }; }; -export const performDefaultAutoLayout = (nodes: Node[], edges: Edge[]): Promise<{ nodes: Node[] }> => { +export const performDefaultAutoLayout = ( + nodes: Node[], + edges: Edge[] +): Promise<{ nodes: Node[] }> => { const layoutOptions: LayoutOptions = { 'elk.algorithm': 'layered', 'elk.layered.spacing.nodeNodeBetweenLayers': '100', @@ -311,10 +320,10 @@ export const performDefaultAutoLayout = (nodes: Node[], edges: Edge[]): Promise< }; export const performAutoLayout = ( - nodes: Node[], + nodes: Node[], edges: Edge[], layoutOptions: LayoutOptions -): Promise<{ nodes: Node[] }> => { +): Promise<{ nodes: Node[] }> => { const graph: ElkNode = { id: 'root', layoutOptions, @@ -335,8 +344,8 @@ export const performAutoLayout = ( 'nodeLabels.placement': '[H_CENTER, V_TOP, INSIDE]', }, }; - if (node.type === 'rectangularNode') { - const rectangularNodeData: RectangularNodeData = node.data as RectangularNodeData; + if (isRectangularNode(node)) { + const rectangularNodeData: RectangularNodeData = node.data; const label = document.querySelector(`[data-id="${rectangularNodeData.label?.id}"]`); if (label) { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts index ddad7bc856..c8e1c7a331 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts @@ -12,7 +12,7 @@ *******************************************************************************/ import { Edge, Node } from 'reactflow'; -import { Diagram } from '../DiagramRenderer.types'; +import { Diagram, NodeData } from '../DiagramRenderer.types'; export interface UseLayoutValue { layout: ( @@ -20,7 +20,7 @@ export interface UseLayoutValue { diagramToLayout: Diagram, callback: (laidoutDiagram: Diagram) => void ) => void; - autoLayout: (nodes: Node[], edges: Edge[]) => Promise<{ nodes: Node[] }>; + autoLayout: (nodes: Node[], edges: Edge[]) => Promise<{ nodes: Node[] }>; } export type Step = 'INITIAL_STEP' | 'BEFORE_LAYOUT' | 'LAYOUT' | 'AFTER_LAYOUT'; 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 index adf466ecd0..a49259f9ec 100644 --- 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 @@ -26,7 +26,6 @@ const iconlabelStyle = ( ): React.CSSProperties => { const iconLabelNodeStyle: React.CSSProperties = { opacity: faded ? '0.4' : '', - marginLeft: '8px', ...style, }; @@ -40,9 +39,11 @@ const iconlabelStyle = ( export const IconLabelNode = memo(({ data, id, selected }: NodeProps) => { const theme = useTheme(); return ( -
- {data.label ?