refactor: improve output selection logic and edge updating in GenericNode (#8254)

* refactor: improve output selection logic and edge updating in GenericNode

* feat: enhance output rendering logic for loop and conditional components

* feat: add group_outputs flag to output components for enhanced display control

* feat: enhance output handling by adding support for hidden outputs in NodeOutputs component

* fix: update display output logic to handle tool mode correctly

* fix: improve output selection logic in NodeOutputs component

* refactor: simplify output selection logic in OutputComponent and GenericNode

* refactor: remove commented code and clean up unused memoization in various components

* refactor: clean up NodeOutputs component by removing commented code and simplifying output logic

* [autofix.ci] apply automated fixes

* feat: add group_outputs field to test_output in schema tests

* test: Skip grouped components output preview test and add wait times for stability

* fix: remove 'group_outputs' property from multiple starter project JSON files

* Revert starter_projects folder to main branch state

* Revert starter_projects folder to match origin/main exactly

* Re-sync starter_projects folder with main branch after merge conflict

* Add "group_outputs" property to various agent configurations

* test: skip progress tracking test for admin users

* test: enable progress tracking for admin users and improve component interaction

* test: remove redundant dropdown interaction and enhance multi-select functionality

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
Deon Sanchez 2025-06-02 12:03:26 -06:00 • committed by GitHub
commit 5c40410f00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 372 additions and 176 deletions

View file

@ -1,4 +1,3 @@
// NodeOutputs.tsx
import { NodeDataType } from "@/types/flow";
import { OutputParameter } from ".";
@ -25,80 +24,86 @@ export default function NodeOutputs({
selectedOutput: any;
handleSelectOutput: any;
}) {
const output = selectedOutput
? outputs.find((output) => output.name === selectedOutput.name)
: outputs[0];
const hasLoopOutput = outputs.some((output) => output.allows_loop);
const hasGroupOutputs = outputs.some((output) => output.group_outputs);
const isConditionalRouter = data.type === "ConditionalRouter";
const hasHiddenOutputs = outputs.some((output) => output.hidden);
if (!output) return null;
const shouldShowAllOutputs =
hasLoopOutput || hasGroupOutputs || isConditionalRouter || hasHiddenOutputs;
const idx =
data.node!.outputs?.findIndex((out) => out.name === output.name) ?? 0;
if (shouldShowAllOutputs) {
const outputsToRender =
keyPrefix === "hidden"
? outputs.filter((output) => output.hidden)
: outputs.filter((output) => !output.hidden);
const isLoop = output?.allows_loop ?? false;
return (
<>
{outputsToRender?.map((output, idx) => (
<OutputParameter
key={`${keyPrefix}-${output.name}-${idx}`}
output={output}
outputs={outputs}
idx={
data.node!.outputs?.findIndex(
(out) => out.name === output.name,
) ?? idx
}
lastOutput={idx === outputsToRender.length - 1}
data={data}
types={types}
selected={selected}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
handleSelectOutput={handleSelectOutput}
hidden={
keyPrefix === "hidden"
? showHiddenOutputs
? output.hidden
: true
: false
}
/>
))}
</>
);
}
const hiddenOutputs = outputs.filter((output) => output.hidden);
const getDisplayOutput = () => {
const filteredOutputs =
keyPrefix === "hidden"
? outputs.filter((output) => output.hidden)
: outputs.filter((output) => !output.hidden);
return isLoop ? (
keyPrefix === "hidden" ? (
hiddenOutputs?.map((output, idx) => (
<OutputParameter
key={`${keyPrefix}-${output.name}-${idx}`}
output={output}
idx={
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
idx
}
lastOutput={idx === outputs.length - 1}
data={data}
types={types}
selected={selected}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
handleSelectOutput={handleSelectOutput}
hidden={
keyPrefix === "hidden"
? showHiddenOutputs
? output.hidden
: true
: false
}
/>
))
) : (
outputs?.map((output, idx) => (
<OutputParameter
key={`${keyPrefix}-${output.name}-${idx}`}
output={output}
idx={
data.node!.outputs?.findIndex((out) => out.name === output.name) ??
idx
}
lastOutput={idx === outputs.length - 1}
data={data}
types={types}
selected={selected}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
handleSelectOutput={handleSelectOutput}
hidden={
keyPrefix === "hidden"
? showHiddenOutputs
? output.hidden
: true
: false
}
/>
))
)
) : (
if (selectedOutput) {
return (
filteredOutputs.find((output) => output.name === selectedOutput.name) ||
filteredOutputs[0]
);
}
const outputWithSelection = filteredOutputs.find(
(output) => output.selected,
);
return outputWithSelection || filteredOutputs[0];
};
const displayOutput = getDisplayOutput();
if (!displayOutput) return null;
return (
<OutputParameter
key={`${keyPrefix}-${output.name}-${idx}`}
output={output}
key={`${keyPrefix}-${displayOutput.name}`}
output={displayOutput}
outputs={outputs}
idx={
data.node!.outputs?.findIndex((out) => out.name === output.name) ?? idx
data.node!.outputs?.findIndex(
(out) => out.name === displayOutput.name,
) ?? 0
}
lastOutput={true}
data={data}
@ -111,7 +116,7 @@ export default function NodeOutputs({
hidden={
keyPrefix === "hidden"
? showHiddenOutputs
? output.hidden
? displayOutput.hidden
: true
: false
}

View file

@ -38,7 +38,6 @@ import OutputComponent from "../OutputComponent";
import HandleRenderComponent from "../handleRenderComponent";
import OutputModal from "../outputModal";
// Memoize IconComponent instances
const EyeIcon = memo(
({ hidden, className }: { hidden: boolean; className: string }) => (
<IconComponent
@ -53,7 +52,6 @@ const SnowflakeIcon = memo(() => (
<IconComponent className="h-5 w-5 text-ice" name="Snowflake" />
));
// Memoize Button components
const HideShowButton = memo(
({
disabled,
@ -178,14 +176,12 @@ function NodeOutputField({
const ref = useRef<HTMLDivElement>(null);
const updateNodeInternals = useUpdateNodeInternals();
// Use selective store subscriptions
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const flowPool = useFlowStore((state) => state.flowPool);
const myData = useTypesStore((state) => state.data);
// Memoize computed values
const { flowPoolId, internalOutputName } = useMemo(() => {
if (data.node?.flow && outputProxy) {
const realOutput = getGroupOutputNodeId(
@ -450,9 +446,7 @@ function NodeOutputField({
errorOutput={errorOutput ?? false}
isToolMode={isToolMode}
title={title}
onClick={() => {
//just to trigger the memoization
}}
onClick={() => {}}
id={data?.type}
/>
</OutputModal>

View file

@ -6,6 +6,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import useFlowStore from "@/stores/flowStore";
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
import { outputComponentType } from "../../../../types/components";
import { cn } from "../../../../utils/utils";
@ -23,6 +24,10 @@ export default function OutputComponent({
handleSelectOutput,
outputName,
}: outputComponentType) {
const nodeType = useFlowStore(
(state) => state.nodes.find((node) => node.id === nodeId)?.data?.type,
);
const displayProxy = (children) => {
if (proxy) {
return (
@ -47,9 +52,17 @@ export default function OutputComponent({
</span>,
);
const hasLoopOutput = outputs?.some?.((output) => output.allows_loop);
const hasGroupOutputs = outputs?.some?.((output) => output.group_outputs);
const isConditionalRouter = nodeType === "ConditionalRouter";
const hasOutputs = outputs.length > 1;
const shouldShowDropdown =
hasOutputs && !hasLoopOutput && !hasGroupOutputs && !isConditionalRouter;
return (
<div>
{outputs.length > 1 ? (
{shouldShowDropdown ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -86,51 +99,4 @@ export default function OutputComponent({
)}
</div>
);
// ! DEACTIVATED UNTIL BETTER IMPLEMENTATION
// return (
// <div className="noflow nopan nodelete nodrag flex items-center gap-2">
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button
// disabled={frozen}
// variant="primary"
// size="xs"
// className={cn(
// frozen ? "text-ice" : "",
// "items-center gap-1 pl-2 pr-1.5 align-middle text-xs font-normal",
// )}
// >
// <span className="pb-px">{selected}</span>
// <ForwardedIconComponent name="ChevronDown" className="h-3 w-3" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent>
// {types.map((type) => (
// <DropdownMenuItem
// onSelect={() => {
// // TODO: UDPDATE SET NODE TO NEW NODE FORM
// setNode(nodeId, (node) => {
// const newNode = cloneDeep(node);
// (newNode.data as NodeDataType).node!.outputs![idx].selected =
// type;
// return newNode;
// });
// updateNodeInternals(nodeId);
// }}
// >
// {type}
// </DropdownMenuItem>
// ))}
// </DropdownMenuContent>
// </DropdownMenu>
// {proxy ? (
// <ShadTooltip content={<span>{proxy.nodeDisplayName}</span>}>
// <span>{name}</span>
// </ShadTooltip>
// ) : (
// <span>{name}</span>
// )}
// </div>
// );
}

View file

@ -54,13 +54,12 @@ const HiddenOutputsButton = memo(
}) => (
<Button
unstyled
className="group flex h-[1.75rem] w-[1.75rem] items-center justify-center rounded-full border bg-muted hover:text-foreground"
className="group flex h-[1.25rem] w-[1.25rem] items-center justify-center rounded-full border bg-muted hover:text-foreground"
onClick={onClick}
>
<ForwardedIconComponent
name={showHiddenOutputs ? "ChevronsDownUp" : "ChevronsUpDown"}
strokeWidth={ICON_STROKE_WIDTH}
className="icon-size text-placeholder-foreground group-hover:text-foreground"
className="h-3 w-3 text-placeholder-foreground group-hover:text-foreground"
/>
</Button>
),
@ -219,7 +218,6 @@ function GenericNode({
const update = useShortcutsStore((state) => state.update);
useHotkeys(update, handleUpdateCodeWShortcut, { preventDefault: true });
// Memoized values
const isToolMode = useMemo(
() =>
data.node?.outputs?.some(
@ -261,35 +259,47 @@ function GenericNode({
}, [data.node?.outputs]);
const [selectedOutput, setSelectedOutput] = useState<OutputFieldType | null>(
null,
() => data.node?.outputs?.find((output) => output.selected) || null,
);
const handleSelectOutput = useCallback(
(output) => {
setSelectedOutput(output);
// Remove any edges connected to this output handle
const sourceHandleId = scapedJSONStringfy({
output_types: [output.selected ?? output.types[0]],
id: data.id,
dataType: data.type,
name: output.name,
});
setEdges((eds) =>
eds.filter((edge) => edge.sourceHandle !== sourceHandleId),
);
setEdges((eds) => {
return eds.map((edge) => {
if (edge.source === data.id && edge.data?.sourceHandle) {
const sourceHandle = edge.data.sourceHandle;
if (sourceHandle.name === output.name) {
const newSourceHandle = {
...sourceHandle,
output_types: [output.selected ?? output.types[0]],
};
const newSourceHandleId = scapedJSONStringfy(newSourceHandle);
return {
...edge,
sourceHandle: newSourceHandleId,
data: {
...edge.data,
sourceHandle: newSourceHandle,
},
};
}
}
return edge;
});
});
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
if (newNode.data.node?.outputs) {
// First, clear any previous selections
newNode.data.node.outputs.forEach((out) => {
if (out.selected) {
out.selected = undefined;
}
});
// Then set the new selection
const outputIndex = newNode.data.node.outputs.findIndex(
(o) => o.name === output.name,
);
@ -566,38 +576,32 @@ function GenericNode({
>
{" "}
</div>
{!showHiddenOutputs && shownOutputs && (
<MemoizedNodeOutputs
outputs={shownOutputs}
keyPrefix={"shown"}
data={data}
types={types}
selected={selected ?? false}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
selectedOutput={selectedOutput}
handleSelectOutput={handleSelectOutput}
/>
{showHiddenOutputs && (
<MemoizedNodeOutputs
outputs={showHiddenOutputs ? hiddenOutputs : shownOutputs}
keyPrefix="shown"
outputs={hiddenOutputs}
keyPrefix="hidden"
data={data}
types={types}
selected={selected ?? false}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
showHiddenOutputs={true}
selectedOutput={selectedOutput}
handleSelectOutput={handleSelectOutput}
/>
)}
<div
className={cn(showHiddenOutputs ? "" : "h-0 overflow-hidden")}
>
<div className="block">
<MemoizedNodeOutputs
outputs={data.node!.outputs}
keyPrefix="hidden"
data={data}
types={types}
selected={selected ?? false}
showNode={showNode}
isToolMode={isToolMode}
showHiddenOutputs={showHiddenOutputs}
selectedOutput={selectedOutput}
handleSelectOutput={handleSelectOutput}
/>
</div>
</div>
{hiddenOutputs && hiddenOutputs.length > 0 && (
<ShadTooltip
content={

View file

@ -113,8 +113,6 @@ const NodeToolbarComponent = memo(
[data.node],
);
// Check if any of the data.node.template fields have tool_mode as True
// if so we can show the tool mode button
const hasToolMode = useMemo(
() => checkHasToolMode(data.node?.template ?? {}) && !isGroup,
[data.node?.template, isGroup],

View file

@ -322,12 +322,6 @@ test(
.getByTestId("handle-pass-shownode-ignored message-left")
.nth(1)
.click();
await page.getByTestId("dropdown-output-conditionalrouter").click();
await page
.getByTestId("dropdown-item-output-conditionalrouter-false")
.click();
await page
.getByTestId("handle-conditionalrouter-shownode-false-right")
.nth(0)