Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3201] Add support for tools on multiple diagram elements (hide, fade… #3202

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
AxelRICHARD marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ image:doc/screenshots/showDeletionConfirmation.png[Deletion Confirmation Dialog,
Thanks to this capability, a specifier can easily retrieve a variable overriden by a child `VariableManager`.
- https://github.com/eclipse-sirius/sirius-web/issues/3096[#3096] [tree] Tree representations can now support filters
- https://github.com/eclipse-sirius/sirius-web/issues/3158[#3158] [diagram] Add a new tool to apply adjust-size on diagram node.
- https://github.com/eclipse-sirius/sirius-web/issues/3201[#3201] [diagram] Add support for tools on multiple diagram elements selection (hide, fade and pin).

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ describe('/projects/:projectId/edit - Robot Diagram', () => {
const fadeByElementTestId = (elementTestId) => {
cy.getByTestId(elementTestId).should('have.css', 'opacity', '1');
cy.getByTestId(elementTestId).first().click({ force: true });
cy.getByTestId('Fade-elements').should('exist').click({ force: true });
cy.getByTestId('Fade-element').should('exist').click({ force: true });
cy.getByTestId(elementTestId).should('have.css', 'opacity', '0.4');
};

const hideByElementTestId = (elementTestId) => {
cy.getByTestId(elementTestId).then((elementBefore) => {
const countBefore = elementBefore.length ?? 0;
cy.getByTestId(elementTestId).first().click({ force: true });
cy.getByTestId('Hide-elements').should('exist').click({ force: true });
cy.getByTestId('Hide-elements').should('not.exist');
cy.getByTestId('Hide-element').should('exist').click({ force: true });
cy.getByTestId('Hide-element').should('not.exist');
if (countBefore === 1) {
cy.getByTestId(elementTestId).should('not.exist');
} else {
Expand Down Expand Up @@ -64,12 +64,12 @@ describe('/projects/:projectId/edit - Robot Diagram', () => {
createFlowReactFlowDiagram();
cy.getByTestId('rf__wrapper').findByTestId('Label - CaptureSubSystem').should('exist').click('topLeft');

cy.getByTestId('Hide-elements').should('exist');
cy.getByTestId('Fade-elements').should('exist');
cy.getByTestId('Hide-element').should('exist');
cy.getByTestId('Fade-element').should('exist');
cy.getByTestId('rf__wrapper').click('bottomLeft');
// NOTE for later: ensure the palette is displayed
cy.getByTestId('Hide-elements').should('not.exist');
cy.getByTestId('Fade-elements').should('not.exist');
cy.getByTestId('Hide-element').should('not.exist');
cy.getByTestId('Fade-element').should('not.exist');
});

it.skip('can fade any type of nodes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { useMoveChange } from './move/useMoveChange';
import { DiagramNodeType } from './node/NodeTypes.types';
import { useNodeType } from './node/useNodeType';
import { DiagramPalette } from './palette/DiagramPalette';
import { GroupPalette } from './palette/group-tool/GroupPalette';
import { useDiagramElementPalette } from './palette/useDiagramElementPalette';
import { useDiagramPalette } from './palette/useDiagramPalette';
import { DiagramPanel } from './panel/DiagramPanel';
Expand All @@ -72,6 +73,7 @@ import { useDiagramSelection } from './selection/useDiagramSelection';
import { useSnapToGrid } from './snap-to-grid/useSnapToGrid';

import 'reactflow/dist/style.css';
import { useGroupPalette } from './palette/group-tool/useGroupPalette';

const GRID_STEP: number = 10;

Expand All @@ -92,6 +94,13 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
isOpened: isDiagramElementPaletteOpened,
} = useDiagramElementPalette();

const {
onDiagramGroupElementClick,
hideGroupPalette,
position: groupPalettePosition,
isOpened: isGroupPaletteOpened,
} = useGroupPalette();

const { onConnect, onConnectStart, onConnectEnd } = useConnector();
const { reconnectEdge } = useReconnectEdge();
const { onDrop, onDragOver } = useDrop();
Expand Down Expand Up @@ -131,8 +140,7 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere

setNodes(laidOutDiagram.nodes);
setEdges(laidOutDiagram.edges);
hideDiagramPalette();
hideDiagramElementPalette();
closeAllPalettes();

synchronizeLayoutData(diagramRefreshedEventPayload.id, laidOutDiagram);
});
Expand Down Expand Up @@ -173,7 +181,7 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
transformedNodeChanges = applyHelperLines(transformedNodeChanges);

if (transformedNodeChanges.some((change) => change.type === 'position')) {
hideDiagramElementPalette();
closeAllPalettes();
}

let newNodes = applyNodeChanges(transformedNodeChanges, oldNodes);
Expand Down Expand Up @@ -228,9 +236,14 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere

const { backgroundColor, smallGridColor, largeGridColor } = diagramBackgroundStyle;

const handleMove: OnMove = useCallback(() => {
const closeAllPalettes = () => {
hideDiagramPalette();
hideDiagramElementPalette();
hideGroupPalette();
};

const handleMove: OnMove = useCallback(() => {
closeAllPalettes();
}, [isDiagramElementPaletteOpened, isDiagramPaletteOpened]);

const handleNodeDragStop: NodeDragHandler = onNodeDragStop((node: Node) => {
Expand All @@ -243,6 +256,18 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
onNodesChange([resetPosition]);
});

const handleDiagramElementCLick = (event: React.MouseEvent<Element, MouseEvent>) => {
onDiagramElementClick(event);
onDiagramGroupElementClick(event);
};

const handleSelectionStart = () => {
closeAllPalettes();
};
const handleSelectionEnd = (event: React.MouseEvent<Element, MouseEvent>) => {
onDiagramGroupElementClick(event);
};

const { onNodeMouseEnter, onNodeMouseLeave } = useNodeHover();

return (
Expand All @@ -261,8 +286,8 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
onEdgesChange={handleEdgesChange}
onEdgeUpdate={reconnectEdge}
onPaneClick={handlePaneClick}
onEdgeClick={onDiagramElementClick}
onNodeClick={onDiagramElementClick}
onEdgeClick={handleDiagramElementCLick}
onNodeClick={handleDiagramElementCLick}
onMove={handleMove}
nodeDragThreshold={1}
onDrop={onDrop}
Expand All @@ -272,6 +297,8 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
onNodeDragStop={handleNodeDragStop}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseLeave={onNodeMouseLeave}
onSelectionStart={handleSelectionStart}
onSelectionEnd={handleSelectionEnd}
maxZoom={40}
minZoom={0.1}
snapToGrid={snapToGrid}
Expand Down Expand Up @@ -307,7 +334,12 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload }: DiagramRendere
onHelperLines={setHelperLinesEnabled}
refreshEventPayloadId={diagramRefreshedEventPayload.id}
/>

<GroupPalette
refreshEventPayloadId={diagramRefreshedEventPayload.id}
x={groupPalettePosition?.x}
y={groupPalettePosition?.y}
isOpened={isGroupPaletteOpened}
/>
<DiagramPalette diagramElementId={diagramRefreshedEventPayload.diagram.id} />
{diagramDescription.debug ? <DebugPanel reactFlowWrapper={ref} /> : null}
<ConnectorContextualMenu />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { PinIcon } from '../../icons/PinIcon';
import { UnpinIcon } from '../../icons/UnpinIcon';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { Tool } from '../Tool';
import { useAdjustSize } from '../adjust-size/useAdjustSize';
import { useFadeDiagramElements } from '../fade/useFadeDiagramElements';
import { useHideDiagramElements } from '../hide/useHideDiagramElements';
import { usePinDiagramElements } from '../pin/usePinDiagramElements';
Expand Down Expand Up @@ -63,7 +64,6 @@ import {
PaletteState,
} from './Palette.types';
import { ToolSection } from './tool-section/ToolSection';
import { useAdjustSize } from '../adjust-size/useAdjustSize';

const usePaletteStyle = makeStyles((theme) => ({
palette: {
Expand Down Expand Up @@ -482,23 +482,23 @@ export const Palette = ({
})}
{hideableDiagramElement ? (
<>
<Tooltip title="Hide elements">
<Tooltip title="Hide element">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="hide elements"
aria-label="Hide element"
onClick={invokeHideDiagramElementTool}
data-testid="Hide-elements">
data-testid="Hide-element">
<VisibilityOffIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Fade elements">
<Tooltip title="Fade element">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Fade elements"
aria-label="Fade element"
onClick={invokeFadeDiagramElementTool}
data-testid="Fade-elements">
data-testid="Fade-element">
<TonalityIcon fontSize="small" />
</IconButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*******************************************************************************
* Copyright (c) 2024 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 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 TonalityIcon from '@material-ui/icons/Tonality';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
import { memo, useCallback, useState } from 'react';
import { useOnSelectionChange } from 'reactflow';
import { PinIcon } from '../../../icons/PinIcon';
import { useFadeDiagramElements } from '../../fade/useFadeDiagramElements';
import { useHideDiagramElements } from '../../hide/useHideDiagramElements';
import { usePinDiagramElements } from '../../pin/usePinDiagramElements';
import { ContextualPaletteStyleProps } from '../Palette.types';
import { PalettePortal } from '../PalettePortal';
import { useDiagramElementPalette } from '../useDiagramElementPalette';
import { GroupPaletteProps } from './GroupPalette.types';

const usePaletteStyle = makeStyles((theme) => ({
palette: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: '2px',
zIndex: 2,
position: 'fixed',
display: 'flex',
alignItems: 'center',
},
paletteContent: {
display: 'grid',
gridTemplateColumns: ({ toolCount }: ContextualPaletteStyleProps) => `repeat(${Math.min(toolCount, 10)}, 36px)`,
gridTemplateRows: '28px',
gridAutoRows: '28px',
placeItems: 'center',
},
toolIcon: {
color: theme.palette.text.primary,
},
}));

export const GroupPalette = memo(({ x, y, isOpened }: GroupPaletteProps) => {
const { hideDiagramElements } = useHideDiagramElements();
const { fadeDiagramElements } = useFadeDiagramElements();
const { pinDiagramElements } = usePinDiagramElements();
const { hideDiagramElementPalette } = useDiagramElementPalette();
const [selectedElementIds, setSelectedElementIds] = useState<string[]>([]);

const onChange = useCallback(({ nodes }) => {
if (nodes.filter((node) => node.selected).length > 1) {
setSelectedElementIds(nodes.filter((node) => node.selected).map((node) => node.id));
} else {
setSelectedElementIds([]);
}
}, []);
useOnSelectionChange({
onChange,
});

const toolCount = 3;
const classes = usePaletteStyle({ toolCount });

const shouldRender = selectedElementIds.length > 1 && isOpened && x && y;
if (!shouldRender) {
return null;
}
hideDiagramElementPalette();

return (
<PalettePortal>
<Paper className={classes.palette} style={{ position: 'absolute', left: x, top: y }} data-testid="GroupPalette">
<div className={classes.paletteContent}>
<Tooltip title="Hide elements">
AxelRICHARD marked this conversation as resolved.
Show resolved Hide resolved
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Hide elements"
onClick={() => hideDiagramElements(selectedElementIds, true)}
data-testid="Hide-elements">
<VisibilityOffIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Fade elements">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Fade elements"
onClick={() => fadeDiagramElements(selectedElementIds, true)}
data-testid="Fade-elements">
<TonalityIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Pin elements">
<IconButton
className={classes.toolIcon}
size="small"
aria-label="Pin elements"
onClick={() => pinDiagramElements(selectedElementIds, true)}
data-testid="Pin-elements">
<PinIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
</Paper>
</PalettePortal>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*******************************************************************************
* Copyright (c) 2024 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
*******************************************************************************/

export interface GroupPaletteProps {
x?: number;
y?: number;
isOpened: boolean;
refreshEventPayloadId: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*******************************************************************************
* Copyright (c) 2024 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 { useCallback, useEffect, useState } from 'react';
import { XYPosition, useKeyPress, useStoreApi } from 'reactflow';
import { UseGroupPaletteValue, UseGroupPaletteState } from './useGroupPalette.types';

const computePalettePosition = (event: MouseEvent | React.MouseEvent, bounds?: DOMRect): XYPosition => {
return {
x: event.clientX - (bounds?.left ?? 0),
y: event.clientY - (bounds?.top ?? 0),
};
};

export const useGroupPalette = (): UseGroupPaletteValue => {
const [state, setState] = useState<UseGroupPaletteState>({ position: null, isOpened: false });

const store = useStoreApi();

const escapePressed = useKeyPress('Escape');
useEffect(() => {
if (escapePressed) {
hideGroupPalette();
}
}, [escapePressed]);

const onDiagramGroupElementClick = useCallback((event: React.MouseEvent<Element, MouseEvent>) => {
const { domNode } = store.getState();
const element = domNode?.getBoundingClientRect();
const palettePosition = computePalettePosition(event, element);
setState((prevState) => ({ ...prevState, position: palettePosition, isOpened: true }));
}, []);

const hideGroupPalette = () => {
setState((prevState) => ({ ...prevState, position: null, isOpened: false }));
};
return {
position: state.position,
isOpened: state.isOpened,
hideGroupPalette,
onDiagramGroupElementClick,
};
};
Loading
Loading