feat(frontend): add support for user password credentials in new FlowEditor (#11122)

- depends on https://github.com/Significant-Gravitas/AutoGPT/pull/11107

In this PR, I’ve added a way to add a username and password as
credentials on new builder.


https://github.com/user-attachments/assets/b896ea62-6a6d-487c-99a3-727cef4ad9a5

### Changes 🏗️
- Introduced PasswordCredentialsModal to handle user password
credentials.
- Updated useCredentialField to support user password type.
- Refactored APIKeyCredentialsModal to remove unnecessary onSuccess
prop.
- Enhanced the CredentialsField component to conditionally render the
new password modal based on supported credential types.

### Checklist 📋

#### For code changes:
- [x] Ability to add username and password correctly.
- [x] The username and password are visible in the credentials list
after adding it.
This commit is contained in:
Abhimanyu Yadav
2025-10-10 12:45:21 +05:30
committed by GitHub
parent df5b348676
commit a2cd5d9c1f
6 changed files with 206 additions and 19 deletions

View File

@@ -6,6 +6,7 @@ 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";
export const CredentialsField = (props: FieldProps) => {
const { formData = {}, onChange, required: _required, schema } = props;
@@ -14,6 +15,7 @@ export const CredentialsField = (props: FieldProps) => {
isCredentialListLoading,
supportsApiKey,
supportsOAuth2,
supportsUserPassword,
credentialsExists,
} = useCredentialField({
credentialSchema: schema as BlockIOCredentialsSubSchema,
@@ -22,6 +24,7 @@ export const CredentialsField = (props: FieldProps) => {
const setField = (key: string, value: any) =>
onChange({ ...formData, [key]: value });
// This is to set the latest credential as the default one [currently, latest means last one in the list of credentials]
useEffect(() => {
if (!isCredentialListLoading && credentials.length > 0 && !formData.id) {
const latestCredential = credentials[credentials.length - 1];
@@ -29,10 +32,6 @@ export const CredentialsField = (props: FieldProps) => {
}
}, [isCredentialListLoading, credentials, formData.id]);
const handleCredentialCreated = (credentialId: string) => {
setField("id", credentialId);
};
if (isCredentialListLoading) {
return (
<div className="flex flex-col gap-2">
@@ -59,12 +58,17 @@ export const CredentialsField = (props: FieldProps) => {
{supportsApiKey && (
<APIKeyCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
onSuccess={handleCredentialCreated}
/>
)}
{supportsOAuth2 && (
<OAuthCredentialModal provider={schema.credentials_provider[0]} />
)}
{supportsUserPassword && (
<PasswordCredentialsModal
schema={schema as BlockIOCredentialsSubSchema}
provider={schema.credentials_provider[0]}
/>
)}
</div>
</div>
);

View File

@@ -14,11 +14,9 @@ import { Text } from "@/components/atoms/Text/Text";
type Props = {
schema: BlockIOCredentialsSubSchema;
onSuccess: (credentialId: string) => void;
};
export function APIKeyCredentialsModal({ schema, onSuccess }: Props) {
export function APIKeyCredentialsModal({ schema }: Props) {
const {
form,
isLoading,
@@ -27,7 +25,7 @@ export function APIKeyCredentialsModal({ schema, onSuccess }: Props) {
provider,
isOpen,
setIsOpen,
} = useAPIKeyCredentialsModal({ schema, onSuccess });
} = useAPIKeyCredentialsModal({ schema });
if (isLoading) {
return null;

View File

@@ -9,7 +9,6 @@ import {
import { useToast } from "@/components/molecules/Toast/use-toast";
import { APIKeyCredentials } from "@/app/api/__generated__/models/aPIKeyCredentials";
import { useQueryClient } from "@tanstack/react-query";
import { PostV1CreateCredentials201 } from "@/app/api/__generated__/models/postV1CreateCredentials201";
import { useState } from "react";
export type APIKeyFormValues = {
@@ -20,12 +19,10 @@ export type APIKeyFormValues = {
type useAPIKeyCredentialsModalType = {
schema: BlockIOCredentialsSubSchema;
onSuccess: (credentialId: string) => void;
};
export function useAPIKeyCredentialsModal({
schema,
onSuccess,
}: useAPIKeyCredentialsModalType): {
form: UseFormReturn<APIKeyFormValues>;
isLoading: boolean;
@@ -42,9 +39,7 @@ export function useAPIKeyCredentialsModal({
const { mutateAsync: createCredentials, isPending: isCreatingCredentials } =
usePostV1CreateCredentials({
mutation: {
onSuccess: async (response) => {
const credentialId = (response.data as PostV1CreateCredentials201)
?.id;
onSuccess: async () => {
form.reset();
setIsOpen(false);
toast({
@@ -56,10 +51,6 @@ export function useAPIKeyCredentialsModal({
await queryClient.refetchQueries({
queryKey: getGetV1ListCredentialsQueryKey(),
});
if (credentialId && onSuccess) {
onSuccess(credentialId);
}
},
onError: () => {
toast({

View File

@@ -0,0 +1,109 @@
import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Form, FormField } from "@/components/__legacy__/ui/form";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
import { usePasswordCredentialModal } from "./usePasswordCredentialModal";
import { toDisplayName } from "../../helpers";
import { UserIcon } from "@phosphor-icons/react";
type Props = {
schema: BlockIOCredentialsSubSchema;
provider: string;
};
export function PasswordCredentialsModal({ schema, provider }: Props) {
const {
credentials,
isCredentialListLoading,
form,
onSubmit,
open,
setOpen,
} = usePasswordCredentialModal({ schema });
if (!credentials || isCredentialListLoading) {
return null;
}
return (
<>
<Dialog
title={`Add new username & password for ${toDisplayName(provider)}`}
controlled={{
isOpen: open,
set: (isOpen) => {
if (!isOpen) setOpen(false);
},
}}
onClose={() => setOpen(false)}
styling={{
maxWidth: "25rem",
}}
>
<Dialog.Content>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-2 pt-4"
>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<Input
id="username"
label="Username"
type="text"
placeholder="Enter username..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<Input
id="password"
label="Password"
type="password"
placeholder="Enter password..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this user login..."
size="small"
{...field}
/>
)}
/>
<Button type="submit" size="small" className="min-w-68">
Save & use this user login
</Button>
</form>
</Form>
</Dialog.Content>
</Dialog>
<Button
type="button"
className="w-fit"
size="small"
onClick={() => setOpen(true)}
>
<UserIcon className="size-4" />
Add username & password
</Button>
</>
);
}

View File

@@ -0,0 +1,82 @@
import { useState } from "react";
import { useCredentialField } from "../../useCredentialField";
import z from "zod";
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
getGetV1ListCredentialsQueryKey,
usePostV1CreateCredentials,
} from "@/app/api/__generated__/endpoints/integrations/integrations";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useQueryClient } from "@tanstack/react-query";
type usePasswordCredentialModalType = {
schema: BlockIOCredentialsSubSchema;
};
export const usePasswordCredentialModal = ({
schema,
}: usePasswordCredentialModalType) => {
const [open, setOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
const { credentials, isCredentialListLoading } = useCredentialField({
credentialSchema: schema,
});
const formSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
title: z.string().min(1, "Name is required"),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
password: "",
title: "",
},
});
const { mutateAsync: createCredentials } = usePostV1CreateCredentials({
mutation: {
onSuccess: async () => {
form.reset();
setOpen(false);
toast({
title: "Success",
description: "Credentials created successfully",
variant: "default",
});
await queryClient.refetchQueries({
queryKey: getGetV1ListCredentialsQueryKey(),
});
},
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
createCredentials({
provider: schema.credentials_provider[0],
data: {
provider: schema.credentials_provider[0],
type: "user_password",
username: values.username,
password: values.password,
title: values.title,
},
});
}
return {
form,
credentials,
isCredentialListLoading,
onSubmit,
open,
setOpen,
};
};

View File

@@ -23,6 +23,8 @@ export const useCredentialField = ({
const supportsApiKey = credentialSchema.credentials_types.includes("api_key");
const supportsOAuth2 = credentialSchema.credentials_types.includes("oauth2");
const supportsUserPassword =
credentialSchema.credentials_types.includes("user_password");
const credentialProviders = credentialSchema.credentials_provider;
const { credentials: filteredCredentials, exists: credentialsExists } =
@@ -33,6 +35,7 @@ export const useCredentialField = ({
isCredentialListLoading,
supportsApiKey,
supportsOAuth2,
supportsUserPassword,
credentialsExists,
};
};