Skip to content

Commit

Permalink
feat(Flow): input and output groups (#3976)
Browse files Browse the repository at this point in the history
* chore(Flow): add node data type in useFlowNodeUtils

* fix(Nodes): parents classes not overridden

* docs(Flow): dashboard node actions moved to footer

* chore(DashboardNode): unnecessary actions container removed

* feat(Flow): inputs/outputs groups and labels as components
  • Loading branch information
MEsteves22 committed Jan 19, 2024
1 parent 4828c1d commit d497ce1
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 156 deletions.
41 changes: 30 additions & 11 deletions apps/app/src/pages/Flow/Nodes/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { css } from "@emotion/css";
import {
HvDashboardNode,
HvFlowNodeFC,
Expand All @@ -10,6 +11,7 @@ import {
HvButton,
HvSection,
HvTypography,
theme,
} from "@hitachivantara/uikit-react-core";

import {
Expand Down Expand Up @@ -43,6 +45,14 @@ const nodeInputs: HvFlowNodeInput[] = [
},
];

const classes = {
footer: css({
display: "flex",
gap: theme.space.sm,
justifyContent: "center",
}),
};

export const Dashboard: HvFlowNodeFC = (props) => {
const { id } = props;

Expand Down Expand Up @@ -117,18 +127,27 @@ export const Dashboard: HvFlowNodeFC = (props) => {
<PreviewRenderer {...item} />
</div>
))}
classes={{
footerContainer: classes.footer,
}}
footer={
<>
<HvButton size="sm" onClick={handleOpenConfig}>
Configure
</HvButton>
<HvButton
size="sm"
variant="primarySubtle"
component="a"
href={`./?dashboard=${id}`}
target="_blank"
>
Preview
</HvButton>
</>
}
{...props}
>
<HvButton onClick={handleOpenConfig}>Configure</HvButton>
<HvButton
variant="primarySubtle"
component="a"
href={`./?dashboard=${id}`}
target="_blank"
>
Preview
</HvButton>
</HvDashboardNode>
/>
);
};

Expand Down
16 changes: 13 additions & 3 deletions packages/lab/src/Flow/DroppableFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { uid } from "uid";

import { ExtractNames, useUniqueId } from "@hitachivantara/uikit-react-core";

import { HvFlowNodeMetaRegistry } from "./types";
import {
HvFlowNodeInputGroup,
HvFlowNodeMetaRegistry,
HvFlowNodeOutputGroup,
} from "./types";
import { staticClasses, useClasses } from "./Flow.styles";
import { useFlowContext } from "./hooks";
import { flowStyles } from "./base";
Expand Down Expand Up @@ -85,8 +89,14 @@ const validateEdge = (
const inputs = nodeMetaRegistry[edge.target]?.inputs || [];
const outputs = nodeMetaRegistry[edge.source]?.outputs || [];

const source = outputs.find((out) => out.id === edge.sourceHandle);
const target = inputs.find((inp) => inp.id === edge.targetHandle);
const source = outputs
.map((out) => (out as HvFlowNodeOutputGroup).outputs || out)
.flat()
.find((out) => out.id === edge.sourceHandle);
const target = inputs
.map((inp) => (inp as HvFlowNodeInputGroup).inputs || inp)
.flat()
.find((inp) => inp.id === edge.targetHandle);

const sourceProvides = source?.provides || "";
const targetAccepts = target?.accepts || [];
Expand Down
12 changes: 12 additions & 0 deletions packages/lab/src/Flow/Node/BaseNode.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ export const { staticClasses, useClasses } = createClasses("HvFlowBaseNode", {
gap: theme.space.xs,
alignItems: "flex-end",
},
inputGroupContainer: {
display: "flex",
flexDirection: "column",
gap: theme.space.xs,
alignItems: "flex-start",
},
outputGroupContainer: {
display: "flex",
flexDirection: "column",
gap: theme.space.xs,
alignItems: "flex-end",
},
inputContainer: {
display: "flex",
flexDirection: "row",
Expand Down
143 changes: 77 additions & 66 deletions packages/lab/src/Flow/Node/BaseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import React, {
useState,
} from "react";
import {
Edge,
Handle,
NodeProps,
NodeToolbar,
Expand All @@ -16,7 +15,6 @@ import {
import { uid } from "uid";
import {
ExtractNames,
HvActionGeneric,
HvBaseProps,
HvButton,
HvTypography,
Expand All @@ -29,6 +27,8 @@ import {
HvFlowBuiltInActions,
HvFlowNodeInput,
HvFlowNodeOutput,
HvFlowNodeOutputGroup,
HvFlowNodeInputGroup,
} from "../types";
import {
useFlowNode,
Expand All @@ -37,6 +37,13 @@ import {
} from "../hooks/useFlowNode";
import { useNodeMetaRegistry } from "../FlowContext/NodeMetaContext";
import { staticClasses, useClasses } from "./BaseNode.styles";
import {
identifyHandles,
isInputConnected,
isInputGroup,
isOutputGroup,
renderedIcon,
} from "./utils";

export { staticClasses as flowBaseNodeClasses };

Expand All @@ -54,9 +61,9 @@ export interface HvFlowBaseNodeProps<T = any>
/** Header items */
headerItems?: React.ReactNode;
/** Node inputs */
inputs?: HvFlowNodeInput[];
inputs?: (HvFlowNodeInput | HvFlowNodeInputGroup)[];
/** Node outputs */
outputs?: HvFlowNodeOutput[];
outputs?: (HvFlowNodeOutput | HvFlowNodeOutputGroup)[];
/** Node actions */
nodeActions?: HvFlowNodeAction[];
/** The content of the Node footer */
Expand All @@ -65,30 +72,11 @@ export interface HvFlowBaseNodeProps<T = any>
classes?: HvFlowBaseNodeClasses;
}

const isInputConnected = (
id: string,
type: "target" | "source",
handleId: string,
edges: Edge[]
) => {
if (type === "target") {
return edges.some((e) => e.target === id && e.targetHandle === handleId);
}
if (type === "source") {
return edges.some((e) => e.source === id && e.sourceHandle === handleId);
}

return false;
};

const defaultActions: HvFlowBuiltInActions[] = [
{ id: "delete", label: "Delete", icon: <Delete /> },
{ id: "duplicate", label: "Duplicate", icon: <Duplicate /> },
];

const renderedIcon = (actionIcon: HvActionGeneric["icon"]) =>
isValidElement(actionIcon) ? actionIcon : (actionIcon as Function)?.();

export const HvFlowBaseNode = ({
id,
title,
Expand All @@ -105,21 +93,9 @@ export const HvFlowBaseNode = ({
}: HvFlowBaseNodeProps<unknown>) => {
const { registerNode, unregisterNode } = useNodeMetaRegistry();

const inputs = useMemo(
() =>
inputsProp?.map((input, idx) =>
input.id != null ? input : { ...input, id: String(idx) }
),
[inputsProp]
);
const inputs = useMemo(() => identifyHandles(inputsProp), [inputsProp]);

const outputs = useMemo(
() =>
outputsProp?.map((output, idx) =>
output.id != null ? output : { ...output, id: String(idx) }
),
[outputsProp]
);
const outputs = useMemo(() => identifyHandles(outputsProp), [outputsProp]);

useEffect(() => {
registerNode(id, {
Expand Down Expand Up @@ -174,6 +150,38 @@ export const HvFlowBaseNode = ({
[node, reactFlowInstance]
);

const renderOutput = (output: HvFlowNodeOutput) => (
<div className={classes.outputContainer} key={output.id}>
<Handle
type="source"
isConnectableEnd={false}
id={output.id}
position={Position.Right}
/>
{output.isMandatory &&
!isInputConnected(id, "source", output.id!, outputEdges) && (
<div className={classes.mandatory} />
)}
<HvTypography component="div">{output.label}</HvTypography>
</div>
);

const renderInput = (input: HvFlowNodeInput) => (
<div className={classes.inputContainer} key={input.id}>
<Handle
type="target"
isConnectableStart={false}
id={input.id}
position={Position.Left}
/>
<HvTypography component="div">{input.label}</HvTypography>
{input.isMandatory &&
!isInputConnected(id, "target", input.id!, inputEdges) && (
<div className={classes.mandatory} />
)}
</div>
);

if (!node) return null;

const color = getColor(colorProp);
Expand Down Expand Up @@ -229,23 +237,24 @@ export const HvFlowBaseNode = ({
<div className={classes.inputsTitleContainer}>
<HvTypography>Inputs</HvTypography>
</div>

<div className={classes.inputsContainer}>
{inputs?.map((input) => (
<div className={classes.inputContainer} key={input.id}>
<Handle
type="target"
isConnectableStart={false}
id={input.id}
position={Position.Left}
/>
<HvTypography>{input.label}</HvTypography>
{input.isMandatory &&
!isInputConnected(id, "target", input.id!, inputEdges) && (
<div className={classes.mandatory} />
{inputs?.map((input, idx) => {
if (!isInputGroup(input)) return renderInput(input);

return (
<div
className={classes.inputGroupContainer}
key={`group${idx}`}
>
<HvTypography component="div" variant="label">
{input.label}
</HvTypography>
{(input as HvFlowNodeInputGroup).inputs.map((inp) =>
renderInput(inp)
)}
</div>
))}
</div>
);
})}
</div>
</>
)}
Expand All @@ -255,21 +264,23 @@ export const HvFlowBaseNode = ({
<HvTypography>Outputs</HvTypography>
</div>
<div className={classes.outputsContainer}>
{outputs?.map((output) => (
<div className={classes.outputContainer} key={output.id}>
<Handle
type="source"
isConnectableEnd={false}
id={output.id}
position={Position.Right}
/>
{output.isMandatory &&
!isInputConnected(id, "source", output.id!, outputEdges) && (
<div className={classes.mandatory} />
{outputs?.map((output, idx) => {
if (!isOutputGroup(output)) return renderOutput(output);

return (
<div
className={classes.outputGroupContainer}
key={`group${idx}`}
>
<HvTypography component="div" variant="label">
{output.label}
</HvTypography>
{(output as HvFlowNodeOutputGroup).outputs.map((out) =>
renderOutput(out)
)}
<HvTypography>{output.label}</HvTypography>
</div>
))}
</div>
);
})}
</div>
</>
)}
Expand Down
8 changes: 8 additions & 0 deletions packages/lab/src/Flow/Node/Node.styles.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { createClasses, theme } from "@hitachivantara/uikit-react-core";

import { staticClasses as baseNodeClasses } from "./BaseNode.styles";

const baseClasses = Object.fromEntries(
Object.keys(baseNodeClasses).map((key) => [key, {}])
) as Record<keyof typeof baseNodeClasses, {}>;

export const { staticClasses, useClasses } = createClasses("HvFlowNode", {
subtitleContainer: {
minHeight: 48,
Expand All @@ -25,4 +31,6 @@ export const { staticClasses, useClasses } = createClasses("HvFlowNode", {
gap: theme.space.xs,
padding: theme.space.sm,
},
// Spread here to know if we are overriding classes from parents
...baseClasses,
});

0 comments on commit d497ce1

Please sign in to comment.