Skip to content

Commit 441adc7

Browse files
Merge pull request #7 from CarstenWickner/enable-editing
2 parents 9a3b967 + c2ba309 commit 441adc7

46 files changed

Lines changed: 2809 additions & 749 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
}
2727
},
2828
"rules": {
29-
"indent": ["error", 4, { "SwitchCase": 1 }],
29+
"indent": "off",
3030
"jsdoc/check-alignment": "warn",
3131
"jsdoc/check-indentation": "warn",
3232
"jsdoc/check-param-names": "error",

package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@
3333
],
3434
"devDependencies": {
3535
"@mdx-js/loader": "~1.5.1",
36-
"@storybook/addon-actions": "~5.3.1",
37-
"@storybook/addon-docs": "~5.3.1",
38-
"@storybook/react": "~5.3.1",
36+
"@storybook/addon-actions": "~5.3.9",
37+
"@storybook/addon-docs": "~5.3.9",
38+
"@storybook/react": "~5.3.9",
3939
"@types/enzyme": "~3.10.3",
4040
"@types/enzyme-adapter-react-16": "~1.0.5",
4141
"@types/jest": "24.0.23",
42+
"@types/lodash.clonedeep": "^4.5.6",
4243
"@types/prop-types": "^15.7.2",
4344
"@types/react": "^16.9.5",
4445
"@types/react-dom": "^16.9.2",
46+
"@types/uuid": "^3.4.6",
4547
"@typescript-eslint/eslint-plugin": "^2.11.0",
4648
"@typescript-eslint/parser": "^2.9.0",
4749
"@typescript-eslint/typescript-estree": "^2.9.0",
@@ -65,7 +67,7 @@
6567
"identity-obj-proxy": "~3.0.0",
6668
"jest": "24.0.0",
6769
"mathsass": "^0.11.0",
68-
"node-sass": "~4.13.0",
70+
"node-sass": "~4.13.1",
6971
"prettier": "~1.19.1",
7072
"react-docgen-typescript-loader": "~3.6.0",
7173
"rollup": "~1.27.0",
@@ -79,7 +81,12 @@
7981
"ts-jest": "~24.2.0",
8082
"typescript": "~3.6.4"
8183
},
82-
"dependencies": {},
84+
"dependencies": {
85+
"lodash.clonedeep": "^4.5.0",
86+
"react-dnd": "^10.0.2",
87+
"react-dnd-html5-backend": "^10.0.2",
88+
"uuid": "^3.4.0"
89+
},
8390
"peerDependencies": {
8491
"prop-types": "^15.7.2",
8592
"react": "^16.9.5",

src/component/ContentElement.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
import * as React from "react";
2+
3+
import { FlowElementWrapper } from "./FlowElementWrapper";
24
import { HorizontalStroke } from "./HorizontalStroke";
5+
import { FlowElementReference } from "../model/FlowElement";
6+
7+
import { onLinkDropCallback } from "../types/EditAction";
8+
9+
export class ContentElement extends React.Component<{
10+
referenceElement: FlowElementReference;
11+
editMenu: (() => React.ReactNode) | undefined;
12+
onLinkDrop: onLinkDropCallback | undefined;
13+
onSelect: (elementId: string) => void;
14+
}> {
15+
onClick = (event: React.MouseEvent): void => {
16+
const { referenceElement, onSelect } = this.props;
17+
onSelect(referenceElement.getId());
18+
event.stopPropagation();
19+
};
320

4-
export const ContentElement: React.FunctionComponent<{
5-
children: React.ReactNode;
6-
}> = ({ children }) => (
7-
<>
8-
<div className="arrow" />
9-
<div className="flow-element content-element">{children}</div>
10-
<HorizontalStroke className="optional" />
11-
</>
12-
);
21+
render(): React.ReactNode {
22+
const { referenceElement, editMenu, onLinkDrop: onDrop, children } = this.props;
23+
return (
24+
<>
25+
<FlowElementWrapper
26+
elementTypeClassName="content-element"
27+
referenceElement={referenceElement}
28+
editMenu={editMenu}
29+
onLinkDrop={onDrop}
30+
onClick={this.onClick}
31+
>
32+
{children}
33+
</FlowElementWrapper>
34+
<HorizontalStroke optional />
35+
</>
36+
);
37+
}
38+
}

src/component/EditMenu.tsx

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as React from "react";
2+
3+
import { EditMenuItem } from "./EditMenuItem";
4+
import { FlowElementReference } from "../model/FlowElement";
5+
import { addContentElement, addDivergingGateway } from "../model/action/addElement";
6+
import { addBranch } from "../model/action/addBranch";
7+
import { isChangeNextElementAllowed } from "../model/action/changeNextElement";
8+
import { removeElement, isRemoveElementAllowed } from "../model/action/removeElement";
9+
10+
import { SelectableElementType, EditActionResult, DraggableType } from "../types/EditAction";
11+
import { FlowModelerProps, MenuOptions } from "../types/FlowModelerProps";
12+
import { ElementType } from "../types/GridCellData";
13+
14+
const onClickStopPropagation = (event: React.MouseEvent): void => event.stopPropagation();
15+
16+
export class EditMenu extends React.Component<{
17+
targetType: SelectableElementType;
18+
referenceElement?: FlowElementReference;
19+
branchIndex?: number;
20+
menuOptions?: FlowModelerProps["options"]["editActions"];
21+
onChange: (change: (originalFlow: FlowModelerProps["flow"]) => EditActionResult) => void;
22+
}> {
23+
isNextElementReferencedByOthers = (): boolean => {
24+
const { referenceElement, branchIndex } = this.props;
25+
if (!referenceElement) {
26+
return false;
27+
}
28+
return referenceElement.getFollowingElements()[branchIndex || 0].getPrecedingElements().length > 1;
29+
};
30+
31+
renderMenuItem(
32+
options: MenuOptions,
33+
defaultClassName: string,
34+
onClick?: (event: React.MouseEvent) => void,
35+
dragType?: DraggableType
36+
): React.ReactNode {
37+
const { targetType, referenceElement, branchIndex } = this.props;
38+
if (options && options.isActionAllowed && !options.isActionAllowed(targetType, referenceElement, branchIndex)) {
39+
return null;
40+
}
41+
const dragItem =
42+
dragType === DraggableType.LINK &&
43+
(targetType === ElementType.Content || targetType == ElementType.ConnectGatewayToElement) &&
44+
this.isNextElementReferencedByOthers()
45+
? { type: dragType, originType: targetType, originElement: referenceElement, originBranchIndex: branchIndex }
46+
: undefined;
47+
return <EditMenuItem key={defaultClassName} options={options} defaultClassName={defaultClassName} onClick={onClick} dragItem={dragItem} />;
48+
}
49+
50+
onAddContentElementClick = (): void => {
51+
const { targetType, onChange, referenceElement, branchIndex } = this.props;
52+
if (targetType !== ElementType.GatewayDiverging) {
53+
onChange((originalFlow) => addContentElement(originalFlow, targetType, {}, referenceElement, branchIndex));
54+
}
55+
};
56+
57+
renderAddContentElementItem(): React.ReactNode {
58+
const { targetType, menuOptions } = this.props;
59+
if (targetType === ElementType.GatewayDiverging) {
60+
return null;
61+
}
62+
return this.renderMenuItem(menuOptions ? menuOptions.addFollowingContentElement : undefined, "add-content", this.onAddContentElementClick);
63+
}
64+
65+
onAddDivergingGatewayClick = (): void => {
66+
const { targetType, onChange, referenceElement, branchIndex } = this.props;
67+
if (targetType !== ElementType.GatewayDiverging) {
68+
onChange((originalFlow) => addDivergingGateway(originalFlow, targetType, {}, referenceElement, branchIndex));
69+
}
70+
};
71+
72+
renderAddDivergingGatewayItem(): React.ReactNode {
73+
const { targetType, menuOptions } = this.props;
74+
if (targetType === ElementType.GatewayDiverging) {
75+
return null;
76+
}
77+
return this.renderMenuItem(
78+
menuOptions ? menuOptions.addFollowingDivergingGateway : undefined,
79+
"add-gateway",
80+
this.onAddDivergingGatewayClick
81+
);
82+
}
83+
84+
onAddDivergingBranchClick = (): void => {
85+
const { targetType, onChange, referenceElement } = this.props;
86+
if (targetType === ElementType.GatewayDiverging) {
87+
onChange((originalFlow) => addBranch(originalFlow, {}, referenceElement));
88+
}
89+
};
90+
91+
renderAddDivergingBranchItem(): React.ReactNode {
92+
const { targetType, menuOptions } = this.props;
93+
if (targetType !== ElementType.GatewayDiverging) {
94+
return null;
95+
}
96+
return this.renderMenuItem(menuOptions ? menuOptions.addDivergingBranch : undefined, "add-branch", this.onAddDivergingBranchClick);
97+
}
98+
99+
renderChangeNextElementItem(): React.ReactNode {
100+
const { targetType, referenceElement, branchIndex, menuOptions } = this.props;
101+
if (isChangeNextElementAllowed(targetType, referenceElement, branchIndex)) {
102+
return this.renderMenuItem(menuOptions ? menuOptions.changeNextElement : undefined, "change-next", undefined, DraggableType.LINK);
103+
}
104+
return null;
105+
}
106+
107+
onRemoveClick = (): void => {
108+
const { targetType, onChange, referenceElement, branchIndex } = this.props;
109+
if (targetType === ElementType.Content || targetType === ElementType.ConnectGatewayToElement) {
110+
onChange((originalFlow) => removeElement(originalFlow, targetType, referenceElement, branchIndex));
111+
}
112+
};
113+
114+
renderRemoveItem(): React.ReactNode {
115+
const { targetType, referenceElement, branchIndex, menuOptions } = this.props;
116+
if (isRemoveElementAllowed(targetType, referenceElement, branchIndex)) {
117+
return this.renderMenuItem(menuOptions ? menuOptions.removeElement : undefined, "remove", this.onRemoveClick);
118+
}
119+
return null;
120+
}
121+
122+
render(): React.ReactNode {
123+
const menuItems = [
124+
this.renderAddContentElementItem(),
125+
this.renderAddDivergingGatewayItem(),
126+
this.renderAddDivergingBranchItem(),
127+
this.renderChangeNextElementItem(),
128+
this.renderRemoveItem()
129+
];
130+
if (menuItems.some((item) => item)) {
131+
return (
132+
<div className="menu" onClick={onClickStopPropagation}>
133+
{menuItems}
134+
</div>
135+
);
136+
}
137+
return null;
138+
}
139+
}

src/component/EditMenuItem.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react";
2+
import { useDrag } from "react-dnd";
3+
4+
import { DraggedLinkContext } from "../types/EditAction";
5+
6+
const disabledDragging: [{ isDragging: boolean }, React.LegacyRef<HTMLSpanElement>] = [{ isDragging: false }, undefined];
7+
8+
export const EditMenuItem: React.FC<{
9+
options: { className?: string; title?: string };
10+
defaultClassName: string;
11+
onClick?: (event: React.MouseEvent) => void;
12+
dragItem?: DraggedLinkContext;
13+
}> = ({ options, defaultClassName, onClick, dragItem }) => {
14+
if (!dragItem && !onClick) {
15+
return null;
16+
}
17+
const [{ isDragging }, drag] = dragItem
18+
? useDrag({
19+
item: dragItem,
20+
collect: (monitor) => ({ isDragging: !!monitor.isDragging() })
21+
})
22+
: disabledDragging;
23+
const baseClassName = (options && options.className) || `menu-item ${defaultClassName}`;
24+
return (
25+
<span
26+
className={`${baseClassName}${isDragging ? " dragging" : ""}`}
27+
{...{ title: options ? options.title : undefined, ref: drag, onClick }}
28+
/>
29+
);
30+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as React from "react";
2+
import { useDrop } from "react-dnd";
3+
4+
import { FlowElementReference } from "../model/FlowElement";
5+
import { DraggableType, DraggedLinkContext, onLinkDropCallback } from "../types/EditAction";
6+
import { isFlowValid } from "../model/pathValidationUtils";
7+
8+
const isTargetAncestorOfDragItem = (originElement?: FlowElementReference, referenceElement?: FlowElementReference): boolean => {
9+
if (!originElement || !referenceElement) {
10+
// reference cannot be ancestor of the start; and end reference cannot be ancestor of anything
11+
return false;
12+
}
13+
if (originElement === referenceElement) {
14+
return true;
15+
}
16+
return originElement.getPrecedingElements().some((preceding) => isTargetAncestorOfDragItem(preceding, referenceElement));
17+
};
18+
19+
const isDropValid = (referenceElement: FlowElementReference, onDrop: onLinkDropCallback) => (dragContext: DraggedLinkContext): boolean => {
20+
if (isTargetAncestorOfDragItem(dragContext.originElement, referenceElement)) {
21+
return false;
22+
}
23+
const currentNextElement = dragContext.originElement.getFollowingElements()[0];
24+
if (currentNextElement === referenceElement || (!referenceElement && currentNextElement.getFollowingElements().length === 0)) {
25+
// linking to the current follower will not change anything
26+
return false;
27+
}
28+
return isFlowValid(onDrop(referenceElement, dragContext, true).changedFlow);
29+
};
30+
31+
const disabledDropping: [{ isOver: boolean; canDrop: false }, React.LegacyRef<HTMLDivElement>] = [{ isOver: false, canDrop: false }, undefined];
32+
33+
export const FlowElementWrapper: React.FC<{
34+
elementTypeClassName: string;
35+
referenceElement?: FlowElementReference;
36+
editMenu?: (() => React.ReactNode) | undefined;
37+
onLinkDrop?: onLinkDropCallback | undefined;
38+
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
39+
}> = ({ elementTypeClassName, referenceElement, editMenu, onLinkDrop: onDrop, onClick, children }) => {
40+
const [{ isOver, canDrop }, drop] = onDrop
41+
? useDrop({
42+
accept: DraggableType.LINK,
43+
canDrop: isDropValid(referenceElement, onDrop),
44+
drop: (dragContext: DraggedLinkContext) => onDrop(referenceElement, dragContext),
45+
collect: (monitor) => ({
46+
isOver: !!monitor.isOver(),
47+
canDrop: !!monitor.canDrop()
48+
})
49+
})
50+
: disabledDropping;
51+
return (
52+
<>
53+
<div className={`stroke-horizontal arrow${isOver && canDrop ? " can-drop" : ""}`} />
54+
<div className={`flow-element-wrapper${isOver ? (canDrop ? " can-drop" : " cannot-drop") : ""}`} ref={drop}>
55+
<div className={`flow-element ${elementTypeClassName}${editMenu ? " selected" : ""}`} onClick={onClick}>
56+
{children}
57+
</div>
58+
{editMenu && editMenu()}
59+
</div>
60+
</>
61+
);
62+
};

0 commit comments

Comments
 (0)