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:
parent
b03ce1ca3d
commit
5c40410f00
44 changed files with 372 additions and 176 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue