Skip to content

Commit

Permalink
Merge pull request #118 from eccenca/feature/handleTools-CMEM-5037
Browse files Browse the repository at this point in the history
Feature: handle tools (CMEM-5037)
  • Loading branch information
andreas-schultz authored Sep 6, 2023
2 parents 53904df + 2f0f13a commit e66f7fc
Show file tree
Hide file tree
Showing 16 changed files with 531 additions and 309 deletions.
3 changes: 2 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": ["stylelint-config-standard-scss", "stylelint-config-recess-order"],
"rules": {
"scss/at-extend-no-missing-placeholder": null
"scss/at-extend-no-missing-placeholder": null,
"selector-class-pattern": null
}
}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Parameter `modalFocusable: boolean`: When `true` the outer `div` element of the modal can be focused by clicking on it.
This is needed e.g. when key (down, up) events should trigger on the modal in order
to bubble up to its parent elements.
- `<HandleTools />`: can be used as single handle content to add an context menu to handles
- `<NodeContent />`
- `introductionTime` parameter could be used to visualize the node was added or updated
- `<ReactFlow />`:
- Support disabling the react-flow hot keys via a React context, e.g. `Delete` etc.

### Fixed

Expand Down
10 changes: 10 additions & 0 deletions src/cmem/react-flow/ReactFlow/ReactFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as graphConfig from "./../configuration/graph";
import * as workflowConfig from "./../configuration/workflow";
import * as linkingConfig from "./../configuration/linking";
import {useReactFlowScrollOnDrag} from "../extensions/scrollOnDragHook";
import {ReactFlowHotkeyContext} from "../extensions/ReactFlowHotkeyContext";

export interface ReactFlowProps extends ReactFlowOriginalProps {
/**
Expand Down Expand Up @@ -42,11 +43,16 @@ export const ReactFlow = React.forwardRef<HTMLDivElement, ReactFlowProps>((
},
ref) => {

/** If the hot keys should be disabled. By default, they are always disabled. */
const {hotKeysDisabled} = React.useContext(ReactFlowHotkeyContext)

const scrollOnDragFunctions = useReactFlowScrollOnDrag({
reactFlowProps: originalProps,
scrollOnDrag
})

const {selectionKeyCode, multiSelectionKeyCode, deleteKeyCode, zoomActivationKeyCode} = originalProps

const configReactFlow = {
unspecified: unspecifiedConfig,
graph: graphConfig,
Expand All @@ -61,6 +67,10 @@ export const ReactFlow = React.forwardRef<HTMLDivElement, ReactFlowProps>((
edgeTypes={ configReactFlow[configuration].edgeTypes }
{...originalProps}
{...scrollOnDragFunctions}
selectionKeyCode={hotKeysDisabled ? null : selectionKeyCode as any}
deleteKeyCode={hotKeysDisabled ? null : deleteKeyCode as any}
multiSelectionKeyCode={hotKeysDisabled ? null : multiSelectionKeyCode as any}
zoomActivationKeyCode={hotKeysDisabled ? null : zoomActivationKeyCode as any}
>
{ children }
<ReactFlowMarkers />
Expand Down
22 changes: 10 additions & 12 deletions src/cmem/react-flow/StickyNoteModal/StickyNoteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { Tag, TagList, SimpleDialog, Icon, Button, FieldItem } from "./../../../index";
import getColorConfiguration from "../../../common/utils/getColorConfiguration";
import { CodeEditor } from "../../../extensions";
import { ReactFlowHotkeyContext } from "../extensions/ReactFlowHotkeyContext";

export type StickyNoteModalTranslationKeys = "modalTitle" | "noteLabel" | "colorLabel" | "saveButton" | "cancelButton";

Expand Down Expand Up @@ -38,24 +39,22 @@ export const StickyNoteModal: React.FC<StickyNoteModalProps> = React.memo(({
const noteColors: [string, string][] = Object.entries(getColorConfiguration("stickynotes")).map(
([key, value]) => [key, value as string]
);
const {disableHotKeys} = React.useContext(ReactFlowHotkeyContext)

React.useEffect(() => {
disableHotKeys(true)

return () => {
disableHotKeys(false)
}
}, [])

React.useEffect(() => {
if (!color && noteColors[0][1]) {
setSelectedColor(noteColors[0][1]);
}
}, [color, noteColors]);

const wrapperDivProps: { [key: string]: (event: any) => any } = {
// Prevent react-flow from getting these events
onContextMenu: (event) => event.stopPropagation(),
onDrag: (event) => event.stopPropagation(),
onDragStart: (event) => event.stopPropagation(),
onDragEnd: (event) => event.stopPropagation(),
onMouseDown: (event) => event.stopPropagation(),
onMouseUp: (event) => event.stopPropagation(),
onClick: (event) => event.stopPropagation(),
};

const predefinedColorsMenu = (
<TagList>
{noteColors &&
Expand Down Expand Up @@ -88,7 +87,6 @@ export const StickyNoteModal: React.FC<StickyNoteModalProps> = React.memo(({
hasBorder
isOpen
onClose={onClose}
wrapperDivProps={wrapperDivProps}
actions={[
<Button
key="submit"
Expand Down
14 changes: 14 additions & 0 deletions src/cmem/react-flow/extensions/ReactFlowHotkeyContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";

export interface ReactFlowHotkeyContextProps {
/** Allows to disable hot keys. */
disableHotKeys: (enable: boolean) => any

/** If the hot keys are currently disabled. */
hotKeysDisabled: boolean
}

export const ReactFlowHotkeyContext = React.createContext<ReactFlowHotkeyContextProps>({
disableHotKeys: () => {},
hotKeysDisabled: false
})
4 changes: 3 additions & 1 deletion src/components/Dialog/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import {
Classes as BlueprintClassNames,
OverlayProps, Overlay as BlueprintOverlay,
IOverlayState,
Overlay as BlueprintOverlay,
OverlayProps,
} from "@blueprintjs/core";
import {Card} from "./../Card";
import {CLASSPREFIX as eccgui} from "../../configuration/constants";
Expand Down Expand Up @@ -87,6 +88,7 @@ export const Modal = ({
// this is a workaround because data attribute on SimpleDialog is not correctly routed to the overlay by blueprint js
data-test-id={(otherProps as any)["data-test-id"] ?? "simpleDialogWidget"}
{...focusableProps}
tabIndex={0}
>
<section
className={
Expand Down
20 changes: 19 additions & 1 deletion src/components/Dialog/SimpleDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, {BaseSyntheticEvent} from "react";

import { IntentTypes } from "../../common/Intent";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
Expand Down Expand Up @@ -71,10 +71,15 @@ export const SimpleDialog = ({
const [displayFullscreen, setDisplayFullscreen] = React.useState<boolean>(startInFullScreenMode);
const showToggler = startInFullScreenMode || showFullScreenToggler;
const intentClassName = intent ? `${eccgui}-intent--${intent}` : "";
const wrapperDivProps = {
...modalPreventEvents,
...otherProps.wrapperDivProps,
};
return (
<Modal
enforceFocus={enforceFocus}
{...otherProps}
wrapperDivProps={wrapperDivProps}
// set default test id if not given
data-test-id={otherProps["data-test-id"] ?? "simpleDialogWidget"}
canOutsideClickClose={canOutsideClickClose || !preventSimpleClosing}
Expand Down Expand Up @@ -116,4 +121,17 @@ export const SimpleDialog = ({
);
};

/** Events that should be prevented to bubble up from a modal that goes beyond the most simple version of a modal, e.g.
* allows to drag or supports hot keys etc. */
export const modalPreventEvents = {
// Prevent certain events from leaving the modal, so that e.g. react-flow does not receive these events doing unexpected stuff
onContextMenu: (event: BaseSyntheticEvent) => event.stopPropagation(),
onDrag: (event: BaseSyntheticEvent) => event.stopPropagation(),
onDragStart: (event: BaseSyntheticEvent) => event.stopPropagation(),
onDragEnd: (event: BaseSyntheticEvent) => event.stopPropagation(),
onMouseDown: (event: BaseSyntheticEvent) => event.stopPropagation(),
onMouseUp: (event: BaseSyntheticEvent) => event.stopPropagation(),
onClick: (event: BaseSyntheticEvent) => event.stopPropagation()
};

export default SimpleDialog;
87 changes: 43 additions & 44 deletions src/extensions/react-flow/edges/stories/EdgeDefault.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import React, { useState, useEffect, useCallback } from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import { ReactFlow, EdgeLabel, EdgeLabelObject } from "./../../../../../index";
import { ArrowHeadType, Elements, getMarkerEnd, ReactFlowProvider } from 'react-flow-renderer';
import React, { useCallback, useEffect, useState } from "react";
import { ArrowHeadType, Elements, getMarkerEnd, ReactFlowProvider } from "react-flow-renderer";
import { ComponentMeta, ComponentStory } from "@storybook/react";

import { EdgeLabel, EdgeLabelObject, ReactFlow } from "./../../../../../index";
import { EdgeDefault, EdgeDefaultDataProps as EdgeData } from "./../EdgeDefault";
import { edgeTypes } from "./../edgeTypes";

const EdgeDefaultDataProps = (data: EdgeData) => {
// this is only a mock to get it as sub element in the table
return <></>;
}
};

export default {
title: "Extensions/React Flow/Default Edge",
title: "Extensions/React Flow/Edge",
component: EdgeDefault,
subcomponents: { EdgeDefaultDataProps },
argTypes: {
type: {
control: "select",
options: [...(Object.keys(edgeTypes))],
options: [...Object.keys(edgeTypes)],
},
},
} as ComponentMeta<typeof EdgeDefault>;
Expand All @@ -33,21 +33,25 @@ const EdgeDefaultExample = (args: any) => {
id: args.source,
type: "default",
data: {
label: "Default ", content: "Example content.", minimalShape: "none",
label: "Default ",
content: "Example content.",
minimalShape: "none",
},
position: { x: 50, y: 200 },
},
{
id: args.target,
type: "default",
data: {
label: "Default ", content: "Example content.", minimalShape: "none",
label: "Default ",
content: "Example content.",
minimalShape: "none",
},
position: { x: 600, y: 200 },
},
{
...args
}
...args,
},
] as Elements);
}, [args]);

Expand All @@ -60,31 +64,30 @@ const EdgeDefaultExample = (args: any) => {
[reactflowInstance]
);

return <ReactFlowProvider>
<ReactFlow
elements={elements}
style={{ height: '400px' }}
onLoad={onLoad}
edgeTypes={ edgeTypes }
defaultZoom={1}
/>
</ReactFlowProvider>
}
return (
<ReactFlowProvider>
<ReactFlow
elements={elements}
style={{ height: "400px" }}
onLoad={onLoad}
edgeTypes={edgeTypes}
defaultZoom={1}
/>
</ReactFlowProvider>
);
};

const Template: ComponentStory<typeof EdgeDefault> = (args) => (
<EdgeDefaultExample {...args} />
);
const Template: ComponentStory<typeof EdgeDefault> = (args) => <EdgeDefaultExample {...args} />;

export const Default = Template.bind({});
Default.args = {
id: 'default',
type: 'dangerStep',
id: "default",
type: "dangerStep",
label: "edge",
arrowHeadType: "arrowclosed",
source: 'node-1',
target: 'node-2',
data: {
}
source: "node-1",
target: "node-2",
data: {},
};

export const CustomLabel = Template.bind({});
Expand All @@ -94,14 +97,12 @@ CustomLabel.args = {
arrowHeadType: undefined,
label: undefined,
data: {
renderLabel: (
edgeCenter: [number, number, number, number]
) => (
renderLabel: (edgeCenter: [number, number, number, number]) => (
<EdgeLabelObject edgeCenter={edgeCenter}>
<EdgeLabel text="Custom label that is very long" />
<EdgeLabel text="Custom label that is very long" />
</EdgeLabelObject>
)
}
),
},
};

export const InverseEdge = Template.bind({});
Expand All @@ -111,32 +112,30 @@ InverseEdge.args = {
arrowHeadType: undefined,
data: {
inversePath: true,
markerStart: getMarkerEnd(
`${ArrowHeadType.ArrowClosed}-inverse` as ArrowHeadType
),
}
markerStart: getMarkerEnd(`${ArrowHeadType.ArrowClosed}-inverse` as ArrowHeadType),
},
};

export const AdjustStrokeType = Template.bind({});
AdjustStrokeType.args = {
...Default.args,
data: {
strokeType: "double",
}
},
};

export const AdjustIntent = Template.bind({});
AdjustIntent.args = {
...Default.args,
data: {
intent: "warning",
}
},
};

export const AdjustHighlight = Template.bind({});
AdjustHighlight.args = {
...Default.args,
data: {
highlightColor: ["default", "alternate"]
}
highlightColor: ["default", "alternate"],
},
};
Loading

0 comments on commit e66f7fc

Please sign in to comment.