fix(frontend): Address PR review comments

- Remove legacy-builder changes: revert BlocksControl.tsx, CustomNode.tsx,
  and Flow.tsx to dev state
- Move MCPToolDialog.tsx from legacy-builder/ to components/ and update
  import path in NewControlPanel/NewBlockMenu/Block.tsx
- Revert out-of-scope CredentialsSelect auto-defaulting behavior
- Remove `as any` cast in CredentialsInput.tsx display name
This commit is contained in:
Zamil Majdy
2026-02-12 18:08:44 +04:00
parent 3ed4d6e56b
commit a4d194cb07
7 changed files with 169 additions and 292 deletions

View File

@@ -13,7 +13,7 @@ import { BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api";
import {
MCPToolDialog,
type MCPToolDialogResult,
} from "@/app/(platform)/build/components/legacy-builder/MCPToolDialog";
} from "@/app/(platform)/build/components/MCPToolDialog";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
title?: string;

View File

@@ -29,10 +29,6 @@ import {
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { GraphMeta } from "@/lib/autogpt-server-api";
import {
MCPToolDialog,
type MCPToolDialogResult,
} from "@/app/(platform)/build/components/legacy-builder/MCPToolDialog";
import jaro from "jaro-winkler";
import { getV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { okData } from "@/app/api/helpers";
@@ -98,7 +94,6 @@ export function BlocksControl({
const [searchQuery, setSearchQuery] = useState("");
const deferredSearchQuery = useDeferredValue(searchQuery);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
const blocks = useSearchableBlocks(_blocks);
@@ -191,32 +186,11 @@ export function BlocksControl({
setSelectedCategory(null);
}, []);
const handleMCPToolConfirm = useCallback(
(result: MCPToolDialogResult) => {
addBlock(SpecialBlockID.MCP_TOOL, "MCPToolBlock", {
server_url: result.serverUrl,
server_name: result.serverName,
selected_tool: result.selectedTool,
tool_input_schema: result.toolInputSchema,
available_tools: result.availableTools,
credentials: result.credentials ?? undefined,
});
setMcpDialogOpen(false);
},
[addBlock],
);
// Handler to add a block, fetching graph data on-demand for agent blocks
const handleAddBlock = useCallback(
async (block: _Block & { notAvailable: string | null }) => {
if (block.notAvailable) return;
// For MCP blocks, open the configuration dialog instead of placing directly
if (block.uiType === BlockUIType.MCP_TOOL) {
setMcpDialogOpen(true);
return;
}
// For agent blocks, fetch the full graph to get schemas
if (block.uiType === BlockUIType.AGENT && block.hardcodedValues) {
const graphID = block.hardcodedValues.graph_id as string;
@@ -256,179 +230,162 @@ export function BlocksControl({
}, [blocks]);
return (
<>
<Popover
open={pinBlocksPopover ? true : undefined}
onOpenChange={(open) => open || resetFilters()}
<Popover
open={pinBlocksPopover ? true : undefined}
onOpenChange={(open) => open || resetFilters()}
>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="blocks-control-popover-trigger"
data-testid="blocks-control-blocks-button"
name="Blocks"
className="dark:hover:bg-slate-800"
>
<IconToyBrick />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Blocks</TooltipContent>
</Tooltip>
<PopoverContent
side="right"
sideOffset={22}
align="start"
className="absolute -top-3 w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
data-id="blocks-control-popover-content"
>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="blocks-control-popover-trigger"
data-testid="blocks-control-blocks-button"
name="Blocks"
className="dark:hover:bg-slate-800"
<Card className="p-3 pb-0 dark:bg-slate-900">
<CardHeader className="flex flex-col gap-x-8 gap-y-1 p-3 px-2">
<div className="items-center justify-between">
<Label
htmlFor="search-blocks"
className="whitespace-nowrap text-base font-bold text-black dark:text-white 2xl:text-xl"
data-id="blocks-control-label"
data-testid="blocks-control-blocks-label"
>
<IconToyBrick />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Blocks</TooltipContent>
</Tooltip>
<PopoverContent
side="right"
sideOffset={22}
align="start"
className="absolute -top-3 w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
data-id="blocks-control-popover-content"
>
<Card className="p-3 pb-0 dark:bg-slate-900">
<CardHeader className="flex flex-col gap-x-8 gap-y-1 p-3 px-2">
<div className="items-center justify-between">
<Label
htmlFor="search-blocks"
className="whitespace-nowrap text-base font-bold text-black dark:text-white 2xl:text-xl"
data-id="blocks-control-label"
data-testid="blocks-control-blocks-label"
>
Blocks
</Label>
</div>
<div className="relative flex items-center">
<MagnifyingGlassIcon className="absolute m-2 h-5 w-5 text-gray-500 dark:text-gray-400" />
<Input
id="search-blocks"
type="text"
placeholder="Search blocks"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="rounded-lg px-8 py-5 dark:bg-slate-800 dark:text-white"
data-id="blocks-control-search-input"
autoComplete="off"
/>
</div>
<div
className="mt-2 flex flex-wrap gap-2"
data-testid="blocks-categories-list"
>
{categories.map((category) => {
const color = getPrimaryCategoryColor([
{ category: category || "All", description: "" },
]);
const colorClass =
selectedCategory === category ? `${color}` : "";
return (
<div
key={category}
data-testid="blocks-category"
role="button"
className={`cursor-pointer rounded-xl border px-2 py-2 text-xs font-medium dark:border-slate-700 dark:text-white ${colorClass}`}
onClick={() =>
setSelectedCategory(
selectedCategory === category ? null : category,
)
}
>
{beautifyString((category || "All").toLowerCase())}
</div>
);
})}
</div>
</CardHeader>
<CardContent className="overflow-scroll border-t border-t-gray-200 p-0 dark:border-t-slate-700">
<ScrollArea
className="h-[60vh] w-full"
data-id="blocks-control-scroll-area"
>
{filteredAvailableBlocks.map((block) => (
<Card
key={block.uiKey || block.id}
className={`m-2 my-4 flex h-20 shadow-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 ${
block.notAvailable
? "cursor-not-allowed opacity-50"
: block.uiType === BlockUIType.MCP_TOOL
? "cursor-pointer hover:shadow-lg"
: "cursor-move hover:shadow-lg"
}`}
data-id={`block-card-${block.id}`}
draggable={
!block.notAvailable &&
block.uiType !== BlockUIType.MCP_TOOL
}
onDragStart={(e) => {
if (
block.notAvailable ||
block.uiType === BlockUIType.MCP_TOOL
Blocks
</Label>
</div>
<div className="relative flex items-center">
<MagnifyingGlassIcon className="absolute m-2 h-5 w-5 text-gray-500 dark:text-gray-400" />
<Input
id="search-blocks"
type="text"
placeholder="Search blocks"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="rounded-lg px-8 py-5 dark:bg-slate-800 dark:text-white"
data-id="blocks-control-search-input"
autoComplete="off"
/>
</div>
<div
className="mt-2 flex flex-wrap gap-2"
data-testid="blocks-categories-list"
>
{categories.map((category) => {
const color = getPrimaryCategoryColor([
{ category: category || "All", description: "" },
]);
const colorClass =
selectedCategory === category ? `${color}` : "";
return (
<div
key={category}
data-testid="blocks-category"
role="button"
className={`cursor-pointer rounded-xl border px-2 py-2 text-xs font-medium dark:border-slate-700 dark:text-white ${colorClass}`}
onClick={() =>
setSelectedCategory(
selectedCategory === category ? null : category,
)
return;
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setData(
"application/reactflow",
JSON.stringify({
blockId: block.id,
blockName: block.name,
hardcodedValues: block?.hardcodedValues || {},
}),
);
}}
onClick={() => handleAddBlock(block)}
title={block.notAvailable ?? undefined}
}
>
<div
className={`-ml-px h-full w-3 rounded-l-xl ${getPrimaryCategoryColor(block.categories)}`}
></div>
{beautifyString((category || "All").toLowerCase())}
</div>
);
})}
</div>
</CardHeader>
<CardContent className="overflow-scroll border-t border-t-gray-200 p-0 dark:border-t-slate-700">
<ScrollArea
className="h-[60vh] w-full"
data-id="blocks-control-scroll-area"
>
{filteredAvailableBlocks.map((block) => (
<Card
key={block.uiKey || block.id}
className={`m-2 my-4 flex h-20 shadow-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 ${
block.notAvailable
? "cursor-not-allowed opacity-50"
: "cursor-move hover:shadow-lg"
}`}
data-id={`block-card-${block.id}`}
draggable={!block.notAvailable}
onDragStart={(e) => {
if (block.notAvailable) return;
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setData(
"application/reactflow",
JSON.stringify({
blockId: block.id,
blockName: block.name,
hardcodedValues: block?.hardcodedValues || {},
}),
);
}}
onClick={() => handleAddBlock(block)}
title={block.notAvailable ?? undefined}
>
<div
className={`-ml-px h-full w-3 rounded-l-xl ${getPrimaryCategoryColor(block.categories)}`}
></div>
<div className="mx-3 flex flex-1 items-center justify-between">
<div className="mr-2 min-w-0">
<span
className="block truncate pb-1 text-sm font-semibold dark:text-white"
data-id={`block-name-${block.id}`}
data-type={block.uiType}
data-testid={`block-name-${block.id}`}
>
<TextRenderer
value={beautifyString(block.name).replace(
/ Block$/,
"",
)}
truncateLengthLimit={45}
/>
</span>
<span
className="block break-all text-xs font-normal text-gray-500 dark:text-gray-400"
data-testid={`block-description-${block.id}`}
>
<TextRenderer
value={block.description}
truncateLengthLimit={165}
/>
</span>
</div>
<div
className="flex flex-shrink-0 items-center gap-1"
data-id={`block-tooltip-${block.id}`}
data-testid={`block-add`}
<div className="mx-3 flex flex-1 items-center justify-between">
<div className="mr-2 min-w-0">
<span
className="block truncate pb-1 text-sm font-semibold dark:text-white"
data-id={`block-name-${block.id}`}
data-type={block.uiType}
data-testid={`block-name-${block.id}`}
>
<PlusIcon className="h-6 w-6 rounded-lg bg-gray-200 stroke-black stroke-[0.5px] p-1 dark:bg-gray-700 dark:stroke-white" />
</div>
<TextRenderer
value={beautifyString(block.name).replace(
/ Block$/,
"",
)}
truncateLengthLimit={45}
/>
</span>
<span
className="block break-all text-xs font-normal text-gray-500 dark:text-gray-400"
data-testid={`block-description-${block.id}`}
>
<TextRenderer
value={block.description}
truncateLengthLimit={165}
/>
</span>
</div>
</Card>
))}
</ScrollArea>
</CardContent>
</Card>
</PopoverContent>
</Popover>
<MCPToolDialog
open={mcpDialogOpen}
onClose={() => setMcpDialogOpen(false)}
onConfirm={handleMCPToolConfirm}
/>
</>
<div
className="flex flex-shrink-0 items-center gap-1"
data-id={`block-tooltip-${block.id}`}
data-testid={`block-add`}
>
<PlusIcon className="h-6 w-6 rounded-lg bg-gray-200 stroke-black stroke-[0.5px] p-1 dark:bg-gray-700 dark:stroke-white" />
</div>
</div>
</Card>
))}
</ScrollArea>
</CardContent>
</Card>
</PopoverContent>
</Popover>
);
}

View File

@@ -215,26 +215,6 @@ export const CustomNode = React.memo(
}
}
// MCP Tool block: display the selected tool's dynamic schema
const isMCPWithTool =
data.uiType === BlockUIType.MCP_TOOL &&
!!data.hardcodedValues?.tool_input_schema?.properties;
if (isMCPWithTool) {
// Show only the tool's input parameters. Credentials are NOT included
// because authentication is handled by the MCP dialog's OAuth flow
// and stored server-side.
const toolSchema = data.hardcodedValues.tool_input_schema;
data.inputSchema = {
type: "object",
properties: {
...(toolSchema.properties ?? {}),
},
required: [...(toolSchema.required ?? [])],
} as BlockIORootSchema;
}
const setHardcodedValues = useCallback(
(values: any) => {
updateNodeData(id, { hardcodedValues: values });
@@ -395,9 +375,7 @@ export const CustomNode = React.memo(
const displayTitle =
customTitle ||
(isMCPWithTool
? `${data.hardcodedValues.server_name || "MCP"}: ${beautifyString(data.hardcodedValues.selected_tool || "")}`
: beautifyString(data.blockType?.replace(/Block$/, "") || data.title));
beautifyString(data.blockType?.replace(/Block$/, "") || data.title);
useEffect(() => {
isInitialSetup.current = false;
@@ -411,15 +389,6 @@ export const CustomNode = React.memo(
data.inputSchema,
),
});
} else if (isMCPWithTool) {
// MCP dialog already configured server_url, selected_tool, etc.
// Just ensure tool_arguments is initialized.
if (!data.hardcodedValues.tool_arguments) {
setHardcodedValues({
...data.hardcodedValues,
tool_arguments: {},
});
}
} else {
setHardcodedValues(
fillObjectDefaultsFromSchema(data.hardcodedValues, data.inputSchema),
@@ -556,11 +525,8 @@ export const CustomNode = React.memo(
);
default:
const getInputPropKey = (key: string) => {
if (nodeType == BlockUIType.AGENT) return `inputs.${key}`;
if (isMCPWithTool) return `tool_arguments.${key}`;
return key;
};
const getInputPropKey = (key: string) =>
nodeType == BlockUIType.AGENT ? `inputs.${key}` : key;
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);

View File

@@ -42,11 +42,7 @@ import { getV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/graphs/
import { okData } from "@/app/api/helpers";
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
import { Key, storage } from "@/services/storage/local-storage";
import {
beautifyString,
findNewlyAddedBlockCoordinates,
getTypeColor,
} from "@/lib/utils";
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
import { history } from "../history";
import { CustomEdge } from "../CustomEdge/CustomEdge";
import ConnectionLine from "../ConnectionLine";
@@ -752,27 +748,6 @@ const FlowEditor: React.FC<{
block_id: blockID,
isOutputStatic: nodeSchema.staticOutput,
uiType: nodeSchema.uiType,
// Set customized_name at creation so it persists through save/load
...(nodeSchema.uiType === BlockUIType.MCP_TOOL && {
metadata: {
credentials_optional: true,
...(finalHardcodedValues.selected_tool && {
customized_name: `${
finalHardcodedValues.server_name ||
(() => {
try {
return new URL(finalHardcodedValues.server_url).hostname;
} catch {
return "MCP";
}
})()
}: ${beautifyString(finalHardcodedValues.selected_tool)}`,
}),
},
}),
...(blockID === SpecialBlockID.AGENT && {
metadata: { customized_name: blockName },
}),
},
};
@@ -902,6 +877,8 @@ const FlowEditor: React.FC<{
return (
node.data.metadata?.customized_name ||
(node.data.uiType == BlockUIType.AGENT &&
node.data.hardcodedValues.agent_name) ||
node.data.blockType.replace(/Block$/, "")
);
},

View File

@@ -86,7 +86,7 @@ export function CredentialsInput({
handleCredentialSelect,
} = hookData;
const displayName = (schema as any).display_name || toDisplayName(provider);
const displayName = toDisplayName(provider);
const selectedCredentialIsSystem =
selectedCredential && isSystemCredential(selectedCredential);

View File

@@ -1,5 +1,4 @@
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { useEffect, useRef } from "react";
import { getCredentialDisplayName } from "../../helpers";
import { CredentialRow } from "../CredentialRow/CredentialRow";
@@ -42,23 +41,17 @@ export function CredentialsSelect({
}
}
// Resolve the selected credential — treat stale/deleted IDs as unselected
const selectedCredential = selectedCredentials
? credentials.find((c) => c.id === selectedCredentials.id)
: null;
// When credentials exist and nothing is matched,
// default to the first credential instead of "None"
const effectiveCredential =
selectedCredential ?? (credentials.length > 0 ? credentials[0] : null);
const displayCredential = effectiveCredential
const displayCredential = selectedCredential
? {
id: effectiveCredential.id,
title: effectiveCredential.title,
username: effectiveCredential.username,
type: effectiveCredential.type,
provider: effectiveCredential.provider,
id: selectedCredential.id,
title: selectedCredential.title,
username: selectedCredential.username,
type: selectedCredential.type,
provider: selectedCredential.provider,
}
: allowNone
? {
@@ -74,37 +67,16 @@ export function CredentialsSelect({
provider: provider,
};
// Use matched credential ID (not the raw selectedCredentials.id which may be stale)
const defaultValue =
effectiveCredential?.id ??
(credentials.length > 0 ? credentials[0].id : "__none__");
// Notify parent when defaulting to a credential so the value is captured on submit
const hasNotifiedDefault = useRef(false);
useEffect(() => {
if (hasNotifiedDefault.current) return;
if (selectedCredential) return; // Already matched — no need to override
if (credentials.length > 0) {
hasNotifiedDefault.current = true;
onSelectCredential(credentials[0].id);
}
}, [credentials, selectedCredential, onSelectCredential]);
return (
<div className="mb-4 w-full">
<div className="relative">
<select
value={defaultValue}
value={selectedCredentials?.id ?? "__none__"}
onChange={handleValueChange}
disabled={readOnly}
className="absolute inset-0 z-10 cursor-pointer opacity-0"
aria-label={`Select ${displayName} credential`}
>
{credentials.map((credential) => (
<option key={credential.id} value={credential.id}>
{getCredentialDisplayName(credential, displayName)}
</option>
))}
{allowNone ? (
<option value="__none__">None (skip this credential)</option>
) : (
@@ -112,6 +84,11 @@ export function CredentialsSelect({
Select a credential
</option>
)}
{credentials.map((credential) => (
<option key={credential.id} value={credential.id}>
{getCredentialDisplayName(credential, displayName)}
</option>
))}
</select>
<div className="rounded-medium border border-zinc-200 bg-white">
<CredentialRow