From 3c689e94a1ff39981522f9a3106ac4926cc30aa4 Mon Sep 17 00:00:00 2001 From: Jeff Phillips Date: Fri, 12 Apr 2024 10:10:40 -0400 Subject: [PATCH] feat(pipelines): Add ability to scale collapsed pipeline groups (#173) * feat(pipelines): Add ability to scale collapsed pipeline groups * Add optional chaining operator to condition * Remove unused import --------- Co-authored-by: Jenny <32821331+jenny-s51@users.noreply.github.com> --- .../pipelineGroupsDemo/DemoTaskGroup.tsx | 61 +++++++--------- .../anchors}/TaskGroupSourceAnchor.ts | 8 ++- .../anchors}/TaskGroupTargetAnchor.ts | 8 ++- .../src/pipelines/components/anchors/index.ts | 2 + .../components/groups/DefaultTaskGroup.tsx | 72 +++++++++++++------ .../groups/DefaultTaskGroupCollapsed.tsx | 34 +++------ .../groups/DefaultTaskGroupExpanded.tsx | 59 +++++++-------- .../src/pipelines/components/groups/index.ts | 4 +- .../pipelines/components/nodes/TaskNode.tsx | 22 ++++-- 9 files changed, 139 insertions(+), 131 deletions(-) rename packages/{demo-app-ts/src/demos/pipelineGroupsDemo => module/src/pipelines/components/anchors}/TaskGroupSourceAnchor.ts (61%) rename packages/{demo-app-ts/src/demos/pipelineGroupsDemo => module/src/pipelines/components/anchors}/TaskGroupTargetAnchor.ts (61%) diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx index 86993844..31c4315e 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx +++ b/packages/demo-app-ts/src/demos/pipelineGroupsDemo/DemoTaskGroup.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import { - AnchorEnd, DagreLayoutOptions, DefaultTaskGroup, GraphElement, @@ -9,27 +8,18 @@ import { LabelPosition, Node, TOP_TO_BOTTOM, - useAnchor, - WithContextMenuProps, WithSelectionProps, - ShapeProps, - WithDragNodeProps, EdgeCreationTypes, + useHover, + ScaleDetailsLevel, + DEFAULT_LAYER, + Layer, + TOP_LAYER, GROUPS_LAYER } from '@patternfly/react-topology'; -import TaskGroupSourceAnchor from './TaskGroupSourceAnchor'; -import TaskGroupTargetAnchor from './TaskGroupTargetAnchor'; type DemoTaskGroupProps = { element: GraphElement; - collapsible?: boolean; - collapsedWidth?: number; - collapsedHeight?: number; - onCollapseChange?: (group: Node, collapsed: boolean) => void; - getCollapsedShape?: (node: Node) => React.FunctionComponent; - collapsedShadowOffset?: number; // defaults to 10 -} & WithContextMenuProps & - WithDragNodeProps & - WithSelectionProps; +} & WithSelectionProps; export const DEFAULT_TASK_WIDTH = 180; export const DEFAULT_TASK_HEIGHT = 32; @@ -41,29 +31,32 @@ const getEdgeCreationTypes = (): EdgeCreationTypes => ({ const DemoTaskGroup: React.FunctionComponent = ({ element, ...rest }) => { const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; + const [hover, hoverRef] = useHover(); + const detailsLevel = element.getGraph().getDetailsLevel(); - useAnchor( - React.useCallback((node: Node) => new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), - AnchorEnd.source - ); - useAnchor( - React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]), - AnchorEnd.target - ); if (!isNode(element)) { return null; } + const groupLayer = element.isCollapsed() ? DEFAULT_LAYER : GROUPS_LAYER; + return ( - + + + + + ); }; diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupSourceAnchor.ts b/packages/module/src/pipelines/components/anchors/TaskGroupSourceAnchor.ts similarity index 61% rename from packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupSourceAnchor.ts rename to packages/module/src/pipelines/components/anchors/TaskGroupSourceAnchor.ts index c082428b..ab9e4e6e 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupSourceAnchor.ts +++ b/packages/module/src/pipelines/components/anchors/TaskGroupSourceAnchor.ts @@ -1,9 +1,11 @@ -import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; +import { AbstractAnchor } from '../../../anchors'; +import { Point } from '../../../geom'; +import { Node } from '../../../types'; -export default class TaskGroupSourceAnchor extends AbstractAnchor { +export default class TaskGroupSourceAnchor extends AbstractAnchor { private vertical = false; - constructor(owner: E, vertical: boolean = true) { + constructor(owner: Node, vertical: boolean = true) { super(owner); this.vertical = vertical; } diff --git a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupTargetAnchor.ts b/packages/module/src/pipelines/components/anchors/TaskGroupTargetAnchor.ts similarity index 61% rename from packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupTargetAnchor.ts rename to packages/module/src/pipelines/components/anchors/TaskGroupTargetAnchor.ts index c349cf72..b2005ccf 100644 --- a/packages/demo-app-ts/src/demos/pipelineGroupsDemo/TaskGroupTargetAnchor.ts +++ b/packages/module/src/pipelines/components/anchors/TaskGroupTargetAnchor.ts @@ -1,9 +1,11 @@ -import { AbstractAnchor, Point, Node } from '@patternfly/react-topology'; +import { AbstractAnchor } from '../../../anchors'; +import { Node } from '../../../types'; +import { Point } from '../../../geom'; -export default class TaskGroupTargetAnchor extends AbstractAnchor { +export default class TaskGroupTargetAnchor extends AbstractAnchor { private vertical = false; - constructor(owner: E, vertical = false) { + constructor(owner: Node, vertical = false) { super(owner); this.vertical = vertical; } diff --git a/packages/module/src/pipelines/components/anchors/index.ts b/packages/module/src/pipelines/components/anchors/index.ts index 6151e954..f2b36405 100644 --- a/packages/module/src/pipelines/components/anchors/index.ts +++ b/packages/module/src/pipelines/components/anchors/index.ts @@ -1,2 +1,4 @@ export { default as TaskNodeSourceAnchor } from './TaskNodeSourceAnchor'; export { default as TaskNodeTargetAnchor } from './TaskNodeTargetAnchor'; +export { default as TaskGroupSourceAnchor } from './TaskGroupSourceAnchor'; +export { default as TaskGroupTargetAnchor } from './TaskGroupTargetAnchor'; diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx index 695827f8..16700adf 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroup.tsx @@ -1,22 +1,28 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { OnSelect, WithDndDragProps, ConnectDragSource, ConnectDropTarget, WithSelectionProps } from '../../../behavior'; +import { + OnSelect, + WithDndDragProps, + ConnectDragSource, + ConnectDropTarget, +} from '../../../behavior'; import { ShapeProps } from '../../../components'; import { Dimensions } from '../../../geom'; import { GraphElement, LabelPosition, BadgeLocation, isNode, Node } from '../../../types'; +import { action } from '../../../mobx-exports'; import { getEdgesFromNodes, getSpacerNodes } from '../../utils'; import DefaultTaskGroupCollapsed from './DefaultTaskGroupCollapsed'; import DefaultTaskGroupExpanded from './DefaultTaskGroupExpanded'; export interface EdgeCreationTypes { - spacerNodeType?: string, + spacerNodeType?: string; edgeType?: string; spacerEdgeType?: string; finallyNodeTypes?: string[]; finallyEdgeType?: string; } -interface PipelinesDefaultGroupProps { +export interface DefaultTaskGroupProps { /** Additional content added to the node */ children?: React.ReactNode; /** Additional classes added to the group */ @@ -33,6 +39,10 @@ interface PipelinesDefaultGroupProps { dragging?: boolean; /** Flag if drag operation is a regroup operation */ dragRegroupable?: boolean; + /** Flag indicating the node should be scaled, best on hover of the node at lowest scale level */ + scaleNode?: boolean; + /** Flag to hide details at medium scale */ + hideDetailsAtMedium?: boolean; /** Flag if the user is hovering on the node */ hover?: boolean; /** Label for the node. Defaults to element.getLabel() */ @@ -45,6 +55,8 @@ interface PipelinesDefaultGroupProps { labelPosition?: LabelPosition; /** The maximum length of the label before truncation */ truncateLength?: number; + /** Space between the label and the group. Defaults to 17 */ + labelOffset?: number; /** The Icon class to show in the label, ignored when labelIcon is specified */ labelIconClass?: string; /** The label icon component to show in the label, takes precedence over labelIconClass */ @@ -73,6 +85,8 @@ interface PipelinesDefaultGroupProps { onCollapseChange?: (group: Node, collapsed: boolean) => void; /** Shape of the collapsed group */ getCollapsedShape?: (node: Node) => React.FunctionComponent; + /** Number of shadows to shop for collapse groups. Defaults to 2 */ + collapsedShadowCount?: number; /** Shadow offset for the collapsed group */ collapsedShadowOffset?: number; /** Flag if the element selected. Part of WithSelectionProps */ @@ -93,7 +107,7 @@ interface PipelinesDefaultGroupProps { recreateLayoutOnCollapseChange?: boolean; /** Function to return types used to re-create edges on a group collapse/expand (should be the same as calls to getEdgesFromNodes) */ getEdgeCreationTypes?: () => { - spacerNodeType?: string, + spacerNodeType?: string; edgeType?: string; spacerEdgeType?: string; finallyNodeTypes?: string[]; @@ -101,13 +115,21 @@ interface PipelinesDefaultGroupProps { }; } -type PipelinesDefaultGroupInnerProps = Omit & { element: Node } & WithSelectionProps; +type PipelinesDefaultGroupInnerProps = Omit & { element: Node }; -const DefaultTaskGroupInner: React.FunctionComponent = observer( - ({ className, element, onCollapseChange, recreateLayoutOnCollapseChange, getEdgeCreationTypes, ...rest }) => { +const DefaultTaskGroupInner: React.FunctionComponent = observer(({ + className, + element, + badge, + onCollapseChange, + collapsedShadowCount, + recreateLayoutOnCollapseChange, + getEdgeCreationTypes, + ...rest +}) => { const childCount = element.getAllNodeChildren().length; - const handleCollapse = (group: Node, collapsed: boolean): void => { + const handleCollapse = action((group: Node, collapsed: boolean): void => { if (collapsed && rest.collapsedWidth !== undefined && rest.collapsedHeight !== undefined) { group.setDimensions(new Dimensions(rest.collapsedWidth, rest.collapsedHeight)); } @@ -120,9 +142,9 @@ const DefaultTaskGroupInner: React.FunctionComponent n.type !== creationTypes.spacerNodeType).map((n) => ({ - ...n, - visible: true - })); + ...n, + visible: true + })); const spacerNodes = getSpacerNodes(pipelineNodes, creationTypes.spacerNodeType, creationTypes.finallyNodeTypes); const nodes = [...pipelineNodes, ...spacerNodes]; const edges = getEdgesFromNodes( @@ -133,24 +155,22 @@ const DefaultTaskGroupInner: React.FunctionComponent ); @@ -161,24 +181,30 @@ const DefaultTaskGroupInner: React.FunctionComponent ); } ); -const DefaultTaskGroup: React.FunctionComponent = ({ +const DefaultTaskGroup: React.FunctionComponent = ({ element, + badgeColor = '#f5f5f5', + badgeBorderColor = '#d2d2d2', + badgeTextColor = '#000000', ...rest -}: PipelinesDefaultGroupProps) => { +}: DefaultTaskGroupProps) => { if (!isNode(element)) { throw new Error('DefaultTaskGroup must be used only on Node elements'); } - return ; + return ; }; export default DefaultTaskGroup; diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx index 54cb2758..4c93100b 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroupCollapsed.tsx @@ -1,38 +1,19 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-alt-icon'; -import { WithDragNodeProps, WithSelectionProps, WithDndDropProps, WithContextMenuProps } from '../../../behavior'; import { CollapsibleGroupProps } from "../../../components"; -import { LabelPosition, BadgeLocation, Node } from '../../../types'; +import { Node } from '../../../types'; import { TaskNode } from '../nodes'; +import { TaskNodeProps } from '../nodes/TaskNode'; -type DefaultTaskGroupCollapsedProps = { - children?: React.ReactNode; - className?: string; +export type DefaultTaskGroupCollapsedProps = { element: Node; - droppable?: boolean; - canDrop?: boolean; - dropTarget?: boolean; - dragging?: boolean; - hover?: boolean; - label?: string; // Defaults to element.getLabel() - secondaryLabel?: string; - showLabel?: boolean; // Defaults to true - labelPosition?: LabelPosition; // Defaults to bottom - truncateLength?: number; // Defaults to 13 - labelIconClass?: string; // Icon to show in label - labelIcon?: string; - labelIconPadding?: number; - badge?: string; - badgeColor?: string; - badgeTextColor?: string; - badgeBorderColor?: string; - badgeClassName?: string; - badgeLocation?: BadgeLocation; -} & CollapsibleGroupProps & WithDragNodeProps & WithSelectionProps & WithDndDropProps & WithContextMenuProps; + shadowCount?: number; +} & Omit & CollapsibleGroupProps; const DefaultTaskGroupCollapsed: React.FunctionComponent = ({ element, + shadowCount = 2, collapsible, onCollapseChange, ...rest @@ -43,7 +24,8 @@ const DefaultTaskGroupCollapsed: React.FunctionComponent : undefined} onActionIconClick={() => onCollapseChange(element, false)} - shadowCount={2} + shadowCount={shadowCount} + {...rest} /> ); }; diff --git a/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx b/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx index 0249a485..3b1adda8 100644 --- a/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx +++ b/packages/module/src/pipelines/components/groups/DefaultTaskGroupExpanded.tsx @@ -7,44 +7,23 @@ import NodeLabel from '../../../components/nodes/labels/NodeLabel'; import { Layer } from '../../../components/layers'; import { GROUPS_LAYER, TOP_LAYER } from '../../../const'; import { maxPadding, useCombineRefs, useHover } from '../../../utils'; -import { BadgeLocation, GraphElement, isGraph, LabelPosition, Node, NodeStyle } from '../../../types'; import { + AnchorEnd, + isGraph, + LabelPosition, + Node, + NodeStyle +} from '../../../types'; +import { + useAnchor, useDragNode, - WithContextMenuProps, - WithDndDropProps, - WithDragNodeProps, - WithSelectionProps } from '../../../behavior'; -import { CollapsibleGroupProps } from '../../../components'; - -type DefaultTaskGroupProps = { - className?: string; - element: GraphElement; - droppable?: boolean; - canDrop?: boolean; - dropTarget?: boolean; - dragging?: boolean; - hover?: boolean; - label?: string; // Defaults to element.getLabel() - secondaryLabel?: string; - showLabel?: boolean; // Defaults to true - labelPosition?: LabelPosition; - truncateLength?: number; // Defaults to 13 - badge?: string; - badgeColor?: string; - badgeTextColor?: string; - badgeBorderColor?: string; - badgeClassName?: string; - badgeLocation?: BadgeLocation; - labelOffset?: number; // Space between the label and the group - labelIconClass?: string; // Icon to show in label - labelIcon?: string; - labelIconPadding?: number; -} & Partial; - -type DefaultTaskGroupInnerProps = Omit & { element: Node }; +import { DagreLayoutOptions, TOP_TO_BOTTOM } from '../../../layouts'; +import TaskGroupSourceAnchor from '../anchors/TaskGroupSourceAnchor'; +import TaskGroupTargetAnchor from '../anchors/TaskGroupTargetAnchor'; +import { DefaultTaskGroupProps } from './DefaultTaskGroup'; -const DefaultTaskGroupExpanded: React.FunctionComponent = observer( +const DefaultTaskGroupExpanded: React.FunctionComponent & { element: Node }> = observer( ({ className, element, @@ -72,7 +51,7 @@ const DefaultTaskGroupExpanded: React.FunctionComponent { const [hovered, hoverRef] = useHover(); const [labelHover, labelHoverRef] = useHover(); @@ -80,6 +59,7 @@ const DefaultTaskGroupExpanded: React.FunctionComponent(hoverRef, dragNodeRef); const isHover = hover !== undefined ? hover : hovered; const labelPosition = element.getLabelPosition(); + const verticalLayout = (element.getGraph().getLayoutOptions?.() as DagreLayoutOptions)?.rankdir === TOP_TO_BOTTOM; let parent = element.getParent(); let altGroup = false; @@ -88,6 +68,15 @@ const DefaultTaskGroupExpanded: React.FunctionComponent new TaskGroupSourceAnchor(node, verticalLayout), [verticalLayout]), + AnchorEnd.source + ); + useAnchor( + React.useCallback((node: Node) => new TaskGroupTargetAnchor(node, verticalLayout), [verticalLayout]), + AnchorEnd.target + ); + const children = element.getNodes().filter((c) => c.isVisible()); // cast to number and coerce diff --git a/packages/module/src/pipelines/components/groups/index.ts b/packages/module/src/pipelines/components/groups/index.ts index 5298db5f..a58f87d3 100644 --- a/packages/module/src/pipelines/components/groups/index.ts +++ b/packages/module/src/pipelines/components/groups/index.ts @@ -1,2 +1,4 @@ -export { default as DefaultTaskGroup } from './DefaultTaskGroup'; export type { EdgeCreationTypes } from './DefaultTaskGroup'; +export { default as DefaultTaskGroup } from './DefaultTaskGroup'; +export { default as DefaultTaskGroupExpanded } from './DefaultTaskGroupExpanded'; +export { default as DefaultTaskGroupCollapsed } from './DefaultTaskGroupCollapsed'; diff --git a/packages/module/src/pipelines/components/nodes/TaskNode.tsx b/packages/module/src/pipelines/components/nodes/TaskNode.tsx index c92599bc..6c1aa404 100644 --- a/packages/module/src/pipelines/components/nodes/TaskNode.tsx +++ b/packages/module/src/pipelines/components/nodes/TaskNode.tsx @@ -233,7 +233,8 @@ const TaskNodeInner: React.FC = observer(({ badgeStartX: 0, iconWidth: 0, iconStartX: 0, - leadIconStartX: 0 + leadIconStartX: 0, + offsetX: 0 }; } const height: number = textHeight + 2 * paddingY; @@ -303,16 +304,25 @@ const TaskNodeInner: React.FC = observer(({ ]); React.useEffect(() => { + const sourceEdges = element.getSourceEdges(); action(() => { - const sourceEdges = element.getSourceEdges(); + const indent = detailsLevel === ScaleDetailsLevel.high && !verticalLayout ? width - pillWidth : 0; sourceEdges.forEach(edge => { const data = edge.getData(); - edge.setData({ - ...(data || {}), - indent: detailsLevel === ScaleDetailsLevel.high && !verticalLayout ? width - pillWidth : 0 - }); + if ((data?.indent ?? 0) !== indent) { + edge.setData({ ...(data || {}), indent }); + } }); })(); + + return action(() => { + sourceEdges.forEach(edge => { + const data = edge.getData(); + if (data?.indent) { + edge.setData({...(data || {}), indent: 0}); + } + }); + }); }, [detailsLevel, element, pillWidth, verticalLayout, width]); const scale = element.getGraph().getScale();