Skip to content

Commit

Permalink
[3774] Add an extension point to contribute custom tools
Browse files Browse the repository at this point in the history
Bug: #3774
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene committed Jul 16, 2024
1 parent 3d3c8db commit 82ba09a
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 152 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
- https://github.com/eclipse-sirius/sirius-web/issues/3702[#3702] [sirius-web] Improve the extensibility of the navigation bar menu by giving consumers of Sirius Web the ability to add new menu entries
- https://github.com/eclipse-sirius/sirius-web/issues/3675[#3675] [core] Remove the label from the selection entry.
+ It also introduces a new hook to retrieve representation metadata

- https://github.com/eclipse-sirius/sirius-web/issues/3774[#3774] [sirius-web] Add an extension point to contribute custom tools

== v2024.7.0

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
= How to contribute custom tool to a Diagram palette
= How to use the extension points to extend the frontend

This document shows the steps needed for an application to contribute its own custom tools.
Sirius-web is an example of an application using the various sirius-component modules.
A user may want to configure certain parts of this application.
Several extension points have been provided to enable a sirius-web-based application to customize its frontend.

An example of a simple tool is available on _Papaya::OperationalActivity_ node to illustrate (see _PapayaOperationActivityLabelDetailToolContribution.tsx_ and _EditProjectView.tsx_).
== How to use the tool extension point

== The tool component
=== The tool component

By contributing a new custom tool, you add a React component to the palette.
This component must use as props `DiagramPaletteToolContributionComponentProps`.
This component must use as props `PaletteToolComponentProps`.
To avoid inconsistency palette representation, we encourage the use of a simple icon, but by definition, you can contribute what you want as a React component.
If needed, this icon can open a modal with more complex UI.

Expand All @@ -19,16 +21,25 @@ To retrieve the _editingContextId_ and the _representationId_, you can use the f
const { projectId: editingContextId, representationId } = useParams<EditProjectViewParams>();
----

== Contribution
=== Add the extension point to the registry

Once the component has been developed, we need to contribute it to the `DiagramPaletteToolContext.Provider`.
To do this, you need to add a `DiagramPaletteToolContribution` to the `DiagramPaletteToolContextValue` array pasted to the provider.
The `DiagramPaletteToolContribution` need two props, the _component_ and a _canHandle_ function, this function will be called each time we create a palette on the diagram.
It must return true only for the palette where the custom tool needs to be added.
To compute that, this function has two parameters the _diagramId_ and the _diagramElementId_, note that for the diagram palette _diagramId_ = _diagramElementId_.
To retrieve the node metadata, you can use the hook `useNodes` and filter all the node by the _diagramElementId_.
To add your custom tool to the application, you have to use `ExtensionRegistry#addComponent` function with `paletteToolExtensionPoint` as `ComponentExtensionPoint`.
By doing so, your component will be added to each diagram palette, most of the time you would like to declare your tool only on certain types of diagrams or nodes.
To do this, it's up to your component to only return something according to the props data.
Only return null in other cases.

== Specify a reference position
For example, to add our papaya tool, we declare the following extension point:

[source,typescript]
----
const papayaExtensionRegistry = new ExtensionRegistry();
papayaExtensionRegistry.addComponent(paletteToolExtensionPoint, {
identifier: 'papaya_customtool',
Component: PapayaOperationActivityLabelDetailToolContribution,
});
----

=== Specify a reference position

Depending on the tool purpose, the click coordinates may be necessary for the result action.
For example, when the tool creates a new element on the diagram.
Expand All @@ -49,4 +60,3 @@ For the generic tool used in sirius-component, the provider is specified in the

The basic case is to use the position of the pallet as a reference position.
These coordinates are available in the props `DiagramPaletteToolContributionComponentProps`.

Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export type { DiagramPaletteToolContextValue } from './renderer/palette/DiagramP
export { DiagramPaletteToolContext } from './renderer/palette/DiagramPaletteToolContext';
export { DiagramPaletteToolContribution } from './renderer/palette/DiagramPaletteToolContribution';
export type { DiagramPaletteToolContributionComponentProps } from './renderer/palette/DiagramPaletteToolContribution.types';
export type { PaletteToolComponentProps } from './renderer/palette/tool/PaletteTool.types';
export type { DiagramPanelActionProps } from './renderer/panel/DiagramPanel.types';
export { paletteToolExtensionPoint } from './renderer/palette/tool/PaletteToolExtensionPoints';
export { diagramPanelActionExtensionPoint } from './renderer/panel/DiagramPanelExtensionPoints';
export { DiagramRepresentation } from './representation/DiagramRepresentation';
export type { GQLDiagramDescription } from './representation/DiagramRepresentation.types';
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import { ToolProps } from './Tool.types';

const useToolStyle = makeStyles(() => ({
const useToolStyle = makeStyles((theme) => ({
toolThumbnail: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: theme.spacing(4.5),
cursor: 'pointer',
},
tool: {
display: 'grid',
gridTemplateRows: '1fr',
gridTemplateColumns: '20px 1fr',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
},
toolLabel: {
marginLeft: theme.spacing(0.5),
},
}));

export const Tool = ({ tool, onClick, thumbnail }: ToolProps) => {
Expand All @@ -34,7 +43,7 @@ export const Tool = ({ tool, onClick, thumbnail }: ToolProps) => {
}
let labelContent: JSX.Element | null = null;
if (!thumbnail) {
labelContent = <Typography>{label}</Typography>;
labelContent = <Typography className={classes.toolLabel}>{label}</Typography>;
}

const onToolClick: React.MouseEventHandler<HTMLDivElement> = (event) => {
Expand All @@ -43,7 +52,11 @@ export const Tool = ({ tool, onClick, thumbnail }: ToolProps) => {
};

return (
<div key={id} className={classes.tool} onClick={onToolClick} data-testid={`${tool.label} - Tool`}>
<div
key={id}
className={thumbnail ? classes.toolThumbnail : classes.tool}
onClick={onToolClick}
data-testid={`${tool.label} - Tool`}>
{image}
{labelContent}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@
*******************************************************************************/

import { gql, useMutation, useQuery } from '@apollo/client';
import { useDeletionConfirmationDialog, useMultiToast } from '@eclipse-sirius/sirius-components-core';
import {
useDeletionConfirmationDialog,
useMultiToast,
useComponents,
ComponentExtension,
} from '@eclipse-sirius/sirius-components-core';
import IconButton from '@material-ui/core/IconButton';
import Paper from '@material-ui/core/Paper';
import Tooltip from '@material-ui/core/Tooltip';
import { makeStyles } from '@material-ui/core/styles';
import AdjustIcon from '@material-ui/icons/Adjust';
import TonalityIcon from '@material-ui/icons/Tonality';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { useReactFlow, useViewport } from 'reactflow';
import { DiagramContext } from '../../contexts/DiagramContext';
import { DiagramContextValue } from '../../contexts/DiagramContext.types';
Expand All @@ -30,11 +35,7 @@ import { Tool } from '../Tool';
import { useAdjustSize } from '../adjust-size/useAdjustSize';
import { useFadeDiagramElements } from '../fade/useFadeDiagramElements';
import { usePinDiagramElements } from '../pin/usePinDiagramElements';
import { DiagramPaletteToolContextValue } from './DiagramPalette.types';
import { DiagramPaletteToolContext } from './DiagramPaletteToolContext';
import { DiagramPaletteToolContributionComponentProps } from './DiagramPaletteToolContribution.types';
import {
ContextualPaletteStyleProps,
GQLCollapsingState,
GQLDeleteFromDiagramData,
GQLDeleteFromDiagramInput,
Expand Down Expand Up @@ -62,24 +63,24 @@ import {
PaletteState,
} from './Palette.types';
import { ToolSection } from './tool-section/ToolSection';
import { paletteToolExtensionPoint } from './tool/PaletteToolExtensionPoints';
import { PaletteToolComponentProps } from './tool/PaletteTool.types';

const usePaletteStyle = makeStyles((theme) => ({
palette: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: '2px',
zIndex: 2,
zIndex: 5,
position: 'fixed',
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
},
paletteContent: {
display: 'grid',
gridTemplateColumns: ({ toolCount }: ContextualPaletteStyleProps) => `repeat(${Math.min(toolCount, 10)}, 36px)`,
gridTemplateRows: '28px',
gridAutoRows: '28px',
placeItems: 'center',
maxWidth: theme.spacing(45.25),
},
toolIcon: {
width: theme.spacing(4.5),
color: theme.palette.text.primary,
},
}));
Expand Down Expand Up @@ -226,9 +227,8 @@ export const Palette = ({
const { addErrorMessage, addMessages } = useMultiToast();
const { showDeletionConfirmation } = useDeletionConfirmationDialog();

const diagramPaletteToolComponents = useContext<DiagramPaletteToolContextValue>(DiagramPaletteToolContext)
.filter((contribution) => contribution.props.canHandle(diagramId, diagramElementId))
.map((contribution) => contribution.props.component);
const paletteToolComponents: ComponentExtension<PaletteToolComponentProps>[] =
useComponents(paletteToolExtensionPoint);

const { data: paletteData, error: paletteError } = useQuery<GQLGetToolSectionsData, GQLGetToolSectionsVariables>(
getPaletteQuery,
Expand All @@ -254,8 +254,8 @@ export const Palette = ({
).length
: 0) +
(hideableDiagramElement ? (node ? 3 : 1) : 0) +
diagramPaletteToolComponents.length;
const classes = usePaletteStyle({ toolCount });
paletteToolComponents.length;
const classes = usePaletteStyle();

let pinUnpinTool: JSX.Element | undefined;
let adjustSizeTool: JSX.Element | undefined;
Expand Down Expand Up @@ -451,45 +451,37 @@ export const Palette = ({
className={classes.palette}
style={{ position: 'absolute', left: paletteX, top: paletteY }}
data-testid="Palette">
<div className={classes.paletteContent}>
{palette?.tools.filter(isSingleClickOnDiagramElementTool).map((tool) => (
<Tool tool={tool} onClick={handleToolClick} thumbnail key={tool.id} />
))}
{palette?.toolSections.map((toolSection) => (
<ToolSection
toolSection={toolSection}
onToolClick={handleToolClick}
key={toolSection.id}
onExpand={handleToolSectionExpand}
toolSectionExpandId={state.expandedToolSectionId}
/>
))}
{diagramPaletteToolComponents.map((component, index) => {
const props: DiagramPaletteToolContributionComponentProps = {
x,
y,
diagramElementId,
key: index.toString(),
};
return React.createElement(component, props);
})}
{hideableDiagramElement ? (
<>
<Tooltip title="Fade element">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Fade element"
onClick={invokeFadeDiagramElementTool}
data-testid="Fade-element">
<TonalityIcon fontSize="small" />
</IconButton>
</Tooltip>
{pinUnpinTool}
{adjustSizeTool}
</>
) : null}
</div>
{palette?.tools.filter(isSingleClickOnDiagramElementTool).map((tool) => (
<Tool tool={tool} onClick={handleToolClick} thumbnail key={tool.id} />
))}
{palette?.toolSections.map((toolSection) => (
<ToolSection
toolSection={toolSection}
onToolClick={handleToolClick}
key={toolSection.id}
onExpand={handleToolSectionExpand}
toolSectionExpandId={state.expandedToolSectionId}
/>
))}
{paletteToolComponents.map(({ Component: PaletteToolComponent }, index) => (
<PaletteToolComponent x={x} y={y} diagramElementId={diagramElementId} key={index} />
))}
{hideableDiagramElement ? (
<>
<Tooltip title="Fade element">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Fade element"
onClick={invokeFadeDiagramElementTool}
data-testid="Fade-element">
<TonalityIcon fontSize="small" />
</IconButton>
</Tooltip>
{pinUnpinTool}
{adjustSizeTool}
</>
) : null}
</Paper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { PalettePortalProps } from './PalettePortal.types';
//The sibling dom element .react-flow__renderer have a zIndex of 4, so we set it here to 5 to have the palette in front of the diagram.
const palettePortalStyle: React.CSSProperties = {
zIndex: 5,
position: 'absolute',
};

export const PalettePortal = ({ children }: PalettePortalProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PaletteToolProps } from './PaletteTool.types';

const usePaletteToolStyle = makeStyles((theme) => ({
toolIcon: {
width: theme.spacing(4.5),
color: theme.palette.text.primary,
},
}));
Expand Down
Loading

0 comments on commit 82ba09a

Please sign in to comment.