Skip to content

Latest commit

 

History

History
184 lines (146 loc) · 5.93 KB

contribute-custom-node.adoc

File metadata and controls

184 lines (146 loc) · 5.93 KB

How to contribute custom node to the Diagram representation

This document shows the steps needed for an application to contribute and use its own custom nodes to Diagram representations.

We will use the example of a "Ellipse" shape to illustrate this.

Custom Node Implementation: Backend

View DSL Integration

While technically optional, this step is highly recommended as it will allow node style to be configurable using the View DSL like the rest of the core node types.

First, create your own Ecore model. It must define sub-types of the NodeStyleDescription EClass from diagram.ecore (org.eclipse.sirius.components.view.diagram.NodeStyleDescription) with the appropriate configuration attributes.

With this information, it becomes possible to create instances of your node’s View-based description class (e.g. EllipseNodeStyle) inside a View-based DiagramDescription.

The final step is to tell the system how to convert these modeled node style description into their corresponding core implementation. This is done by declaring a INodeStyleProvider:

@Service
public class EllipseNodeStyleProvider implements INodeStyleProvider {
     @Override
    public Optional<String> getNodeType(NodeStyleDescription nodeStyle) {
        if (nodeStyle instanceof EllipseNodeStyleDescription) {
            return Optional.of(NODE_ELLIPSE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<INodeStyle> createNodeStyle(NodeStyleDescription nodeStyle, Optional<String> optionalEditingContextId) {
        // ...
        return Optional.of(EllipseNodeStyle.newEllipseNodeStyle().build());
    }
}

You will need to declare your new nodeStyle in the global GraphQL Schema. This is done by simply providing a .graphqls schema file. Note that the file must be in a schema folder in the classpath to be detected. For the Ellipse node we have /sirius-web-sample-application/src/main/resources/schema/customnodes.graphqls with the following content:

type EllipseNodeStyle {
  borderColor: String!
  borderSize: Int!
  borderStyle: LineStyle!
  color: String!
}

extend union INodeStyle = EllipseNodeStyle
Important
Make sure the type of the nodeStyle declared in the GraphQL Schema (type EllipseNodeStyle above) matches the name of the Java class implementing the node (EllipseNodeStyle.java).

Custom Node Implementation: Frontend

  • Declare the GQLNodeStyle concrete sub-type for you new node:

// File: packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.types.ts
import { GQLNodeStyle } from '@eclipse-sirius/sirius-components-diagrams-reactflow';

export interface GQLEllipseNodeStyle extends GQLNodeStyle {
  color: string;
  borderColor: string;
  borderStyle: string;
  borderSize: string;
}
  • Implement the actual frontend React-flow node type for your new shape.

// File: packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNode.tsx
export const EllipseNode = memo(({ data, isConnectable, id, selected }: NodeProps<EllipseNodeData>) => {
  // ...
});
  • Implement the handler in charge of the conversion.

// File: packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeConverterHandler.tsx
export class EllipseNodeConverterHandler implements INodeConverterHandler {
  // ...
}

The canHandle predicate, check on what gqlNode the converter should be used.

canHandle(gqlNode: GQLNode) {
  return gqlNode.style.__typename === 'EllipseNodeStyle';
}

The handle method, converts a GQLNode to a concrete Node and adds it to the nodes array, remember to convert borderNodes and childNodes if necessary.

  handle(
    convertEngine: IConvertEngine,
    gqlNode: GQLNode,
    parentNode: GQLNode | null,
    isBorderNode: boolean,
    nodes: Node[]
  ) {
    nodes.push(toEllipseNode(gqlNode, parentNode, isBorderNode));
    convertEngine.convertNodes(gqlNode.borderNodes ?? [], gqlNode, nodes);
    convertEngine.convertNodes(gqlNode.childNodes ?? [], gqlNode, nodes);
  }
  • Implement the handler in charge of the layout.

export class EllipseNodeLayoutHandler implements INodeLayoutHandler<NodeData> {
// ...
}

The canHandle predicate, check on what node the layout should be used.

canHandle(node: Node<NodeData, DiagramNodeType>) {
  return node.type === 'ellipseNode';
}

The handle method, layout a node by setting its size and position, remember to take into account borderNodes and childNodes if necessary.

  handle(
    layoutEngine: ILayoutEngine,
    previousDiagram: Diagram | null,
    node: Node<NodeData>,
    visibleNodes: Node<NodeData, DiagramNodeType>[],
    directChildren: Node<NodeData, DiagramNodeType>[],
    forceWidth?: number
  ) {
    // ...
  }
  • Finally, you have to contribute all these elements to the NodeTypeComponentRegistry, for sirius-web this registry is in packages/sirius-web/frontend/sirius-web/src/views/edit-project/EditProjectView.tsx.

    const nodeTypeRegistry: NodeTypeComponentRegistry = {
      getNodeStyleContributions: () => {
        const ellipseNodeStyleContribution: NodeStyleContribution = {
          name: 'EllipseNodeStyle',
          fields: `borderColor, borderSize, borderStyle, color`,
        };
        return [ellipseNodeStyleContribution];
      },
      getNodeLayoutHandler: () => {
        return [new EllipseNodeLayoutHandler()];
      },
      getNodeConverterHandler: () => {
        return [new EllipseNodeConverterHandler()];
      },
      getNodeTypeContributions: () => {
        return [<NodeTypeContribution component={EllipseNode} type={'ellipseNode'} />];
      },
    };
Important
Make sure the type of the getNodeTypeContributions matches the type set during the conversion (in EllipseNodeConverterHandler).