feat(frontend): add "None" option for optional credentials in run dialog

Allow users to skip optional credentials by selecting "None" in the
credential dropdown. This properly surfaces the backend optional
credentials flag in the run dialog UI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-08 02:37:34 -07:00
parent 477eeb211e
commit cc14d8527d
5 changed files with 42 additions and 134 deletions

View File

@@ -56,6 +56,7 @@ export function CredentialsInput({
siblingInputs,
onLoaded,
readOnly,
isOptional,
});
if (!isLoaded(hookData)) {
@@ -112,14 +113,16 @@ export function CredentialsInput({
{hasCredentialsToShow ? (
<>
{credentialsToShow.length > 1 && !readOnly ? (
{(credentialsToShow.length > 1 || isOptional) && !readOnly ? (
<CredentialsSelect
credentials={credentialsToShow}
provider={provider}
displayName={displayName}
selectedCredentials={selectedCredential}
onSelectCredential={handleCredentialSelect}
onClearCredential={() => onSelectCredential(undefined)}
readOnly={readOnly}
allowNone={isOptional}
/>
) : (
<div className="mb-4 space-y-2">

View File

@@ -23,7 +23,9 @@ interface Props {
displayName: string;
selectedCredentials?: CredentialsMetaInput;
onSelectCredential: (credentialId: string) => void;
onClearCredential?: () => void;
readOnly?: boolean;
allowNone?: boolean;
}
export function CredentialsSelect({
@@ -32,20 +34,30 @@ export function CredentialsSelect({
displayName,
selectedCredentials,
onSelectCredential,
onClearCredential,
readOnly = false,
allowNone = true,
}: Props) {
// Auto-select first credential if none is selected
// Auto-select first credential if none is selected (only if allowNone is false)
useEffect(() => {
if (!selectedCredentials && credentials.length > 0) {
if (!allowNone && !selectedCredentials && credentials.length > 0) {
onSelectCredential(credentials[0].id);
}
}, [selectedCredentials, credentials, onSelectCredential]);
}, [allowNone, selectedCredentials, credentials, onSelectCredential]);
const handleValueChange = (value: string) => {
if (value === "__none__") {
onClearCredential?.();
} else {
onSelectCredential(value);
}
};
return (
<div className="mb-4 w-full">
<Select
value={selectedCredentials?.id || ""}
onValueChange={(value) => onSelectCredential(value)}
value={selectedCredentials?.id || (allowNone ? "__none__" : "")}
onValueChange={handleValueChange}
>
<SelectTrigger className="h-auto min-h-12 w-full rounded-medium border-zinc-200 p-0 pr-4 shadow-none">
{selectedCredentials ? (
@@ -70,6 +82,15 @@ export function CredentialsSelect({
)}
</SelectTrigger>
<SelectContent>
{allowNone && (
<SelectItem key="__none__" value="__none__">
<div className="flex items-center gap-2">
<Text variant="body" className="tracking-tight text-gray-500">
None (skip this credential)
</Text>
</div>
</SelectItem>
)}
{credentials.map((credential) => (
<SelectItem key={credential.id} value={credential.id}>
<div className="flex items-center gap-2">

View File

@@ -22,6 +22,7 @@ type Params = {
siblingInputs?: Record<string, any>;
onLoaded?: (loaded: boolean) => void;
readOnly?: boolean;
isOptional?: boolean;
};
export function useCredentialsInput({
@@ -31,6 +32,7 @@ export function useCredentialsInput({
siblingInputs,
onLoaded,
readOnly = false,
isOptional = false,
}: Params) {
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
useState(false);
@@ -99,13 +101,20 @@ export function useCredentialsInput({
: null;
}, [credentials]);
// Auto-select the one available credential
// Auto-select the one available credential (only if not optional)
useEffect(() => {
if (readOnly) return;
if (isOptional) return; // Don't auto-select when credential is optional
if (singleCredential && !selectedCredential) {
onSelectCredential(singleCredential);
}
}, [singleCredential, selectedCredential, onSelectCredential, readOnly]);
}, [
singleCredential,
selectedCredential,
onSelectCredential,
readOnly,
isOptional,
]);
if (
!credentials ||

View File

@@ -87,6 +87,7 @@ export const CredentialsField = (props: FieldProps) => {
siblingInputs={hardcodedValues}
showTitle={false}
readOnly={formContext?.readOnly}
isOptional={!isRequired}
/>
{/* Optional credentials toggle - only show in builder canvas, not run dialogs */}

View File

@@ -1,126 +0,0 @@
import React from "react";
import { FieldProps } from "@rjsf/utils";
import { useCredentialField } from "./useCredentialField";
import { SelectCredential } from "./SelectCredential";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { APIKeyCredentialsModal } from "./models/APIKeyCredentialModal/APIKeyCredentialModal";
import { OAuthCredentialModal } from "./models/OAuthCredentialModal/OAuthCredentialModal";
import { PasswordCredentialsModal } from "./models/PasswordCredentialModal/PasswordCredentialModal";
import { HostScopedCredentialsModal } from "./models/HostScopedCredentialsModal/HostScopedCredentialsModal";
import { Switch } from "@/components/atoms/Switch/Switch";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { useShallow } from "zustand/react/shallow";
export const CredentialsField = (props: FieldProps) => {
const {
formData = {},
onChange,
required: _required,
schema,
formContext,
} = props;
const nodeId = formContext.nodeId;
// Only show the optional toggle when editing blocks in the builder canvas
const showOptionalToggle = formContext.showOptionalToggle !== false && nodeId;
const { credentialsOptional, setCredentialsOptional } = useNodeStore(
useShallow((state) => ({
credentialsOptional: state.getCredentialsOptional(nodeId),
setCredentialsOptional: state.setCredentialsOptional,
})),
);
const {
credentials,
isCredentialListLoading,
supportsApiKey,
supportsOAuth2,
supportsUserPassword,
supportsHostScoped,
credentialsExists,
credentialProvider,
setCredential,
discriminatorValue,
} = useCredentialField({
credentialSchema: schema as BlockIOCredentialsSubSchema,
formData,
nodeId,
onChange,
disableAutoSelect: credentialsOptional,
});
if (isCredentialListLoading) {
return (
<div className="flex flex-col gap-2">
<Skeleton className="h-8 w-full rounded-xlarge" />
<Skeleton className="h-8 w-[30%] rounded-xlarge" />
</div>
);
}
if (!credentialProvider) {
return null;
}
return (
<div className="flex flex-col gap-2">
{credentialsExists && (
<SelectCredential
credentials={credentials}
value={formData.id || ""}
onChange={setCredential}
disabled={false}
label="Credential"
placeholder={
credentialsOptional
? "Select credential (optional)"
: "Select credential"
}
/>
)}
<div className="flex flex-wrap gap-2">
{supportsApiKey && (
<APIKeyCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
provider={credentialProvider}
/>
)}
{supportsOAuth2 && (
<OAuthCredentialModal provider={credentialProvider} />
)}
{supportsUserPassword && (
<PasswordCredentialsModal provider={credentialProvider} />
)}
{supportsHostScoped && discriminatorValue && (
<HostScopedCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
provider={credentialProvider}
discriminatorValue={discriminatorValue}
/>
)}
</div>
{/* Optional credentials toggle - only show in builder canvas, not run dialogs */}
{showOptionalToggle && (
<div className="mt-1 flex items-center gap-2">
<Switch
id={`credentials-optional-${nodeId}`}
checked={credentialsOptional}
onCheckedChange={(checked) =>
setCredentialsOptional(nodeId, checked)
}
/>
<label
htmlFor={`credentials-optional-${nodeId}`}
className="cursor-pointer text-xs text-gray-500"
>
Optional - skip block if not configured
</label>
</div>
)}
</div>
);
};