refactor(onboarding): extract useCreateAgent and useDeleteAgent hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Théo LAGACHE
2026-06-19 10:31:21 +02:00
co-authored by Claude Sonnet 4.6
parent 808e60557b
commit 8c1025ece9
3 changed files with 129 additions and 104 deletions
@@ -0,0 +1,41 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { useOnboarding } from "@onboardjs/react";
import { toast } from "sonner";
import { createAgentAction } from "@/features/agents/agents.action";
import type { OnboardingAgent, OnboardingDefaultsData } from "@/features/onboarding/types";
export const useCreateAgent = () => {
const { state, updateContext } = useOnboarding();
return useMutation({
mutationFn: async (name: string) => {
const orgId = (state?.context.flowData.org as any)?.id as string | undefined;
if (!orgId) throw new Error("Missing org ID — cannot create agent");
const defaults = (state?.context.flowData.defaults ?? {}) as OnboardingDefaultsData;
const result = await createAgentAction({
organizationId: orgId,
data: { name, description: "" },
});
if (!result?.data?.data) {
throw new Error(result?.serverError ?? `Failed to create agent "${name}"`);
}
const newAgent: OnboardingAgent = {
id: result.data.data.id,
name: result.data.data.name,
notifierId: defaults.notifierId,
storageId: defaults.storageId,
};
const agents = [
...((state?.context.flowData.agents ?? []) as OnboardingAgent[]),
newAgent,
];
await updateContext({ flowData: { ...state?.context.flowData, agents } });
return newAgent;
},
onError: (err: Error) => toast.error(err.message),
});
};
@@ -0,0 +1,25 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { useOnboarding } from "@onboardjs/react";
import { toast } from "sonner";
import { deleteAgentAction } from "@/features/agents/agent-delete.action";
import type { OnboardingAgent } from "@/features/onboarding/types";
export const useDeleteAgent = () => {
const { state, updateContext } = useOnboarding();
return useMutation({
mutationFn: async (agentId: string) => {
const orgId = (state?.context.flowData.org as any)?.id as string | undefined;
const result = await deleteAgentAction({ agentId, organizationId: orgId });
if (result?.data?.success === false) throw new Error("Failed to delete agent");
const agents = (
(state?.context.flowData.agents ?? []) as OnboardingAgent[]
).filter((a) => a.id !== agentId);
await updateContext({ flowData: { ...state?.context.flowData, agents } });
},
onError: (err: Error) => toast.error(err.message),
});
};
@@ -2,118 +2,77 @@
import { useState } from "react";
import { useOnboarding } from "@onboardjs/react";
import { toast } from "sonner";
import { X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { createAgentAction } from "@/features/agents/agents.action";
import { deleteAgentAction } from "@/features/agents/agent-delete.action";
import type { OnboardingAgent, OnboardingDefaultsData } from "@/features/onboarding/types";
import { useCreateAgent } from "@/features/onboarding/hooks/use-create-agent";
import { useDeleteAgent } from "@/features/onboarding/hooks/use-delete-agent";
import type { OnboardingAgent } from "@/features/onboarding/types";
export const StepAgentCreate = () => {
const { next, updateContext, state } = useOnboarding();
const defaults = (state?.context.flowData.defaults ?? {}) as OnboardingDefaultsData;
const agents = (state?.context.flowData.agents ?? []) as OnboardingAgent[];
const orgId = (state?.context.flowData.org as any)?.id as string | undefined;
const { next, state } = useOnboarding();
const agents = (state?.context.flowData.agents ?? []) as OnboardingAgent[];
const [name, setName] = useState("");
const createAgent = useCreateAgent();
const deleteAgent = useDeleteAgent();
const [name, setName] = useState("");
const [adding, setAdding] = useState(false);
const onAdd = () => {
if (!name.trim()) return;
createAgent.mutate(name.trim(), { onSuccess: () => setName("") });
};
const addAgent = async () => {
if (!name.trim()) return;
if (!orgId) {
toast.error("Missing org ID — cannot create agent");
return;
}
setAdding(true);
try {
const result = await createAgentAction({
organizationId: orgId,
data: { name: name.trim(), description: "" },
});
if (!result?.data?.data) {
toast.error(result?.serverError ?? `Failed to create agent "${name.trim()}"`);
return;
}
const newAgent: OnboardingAgent = {
id: result.data.data.id,
name: result.data.data.name,
notifierId: defaults.notifierId,
storageId: defaults.storageId,
};
const updated = [...agents, newAgent];
setName("");
await updateContext({ flowData: { ...state?.context.flowData, agents: updated } });
} finally {
setAdding(false);
}
};
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Create an agent</h1>
<p className="text-sm text-muted-foreground mt-1">
Optional agents will use your default notifier and storage.
</p>
</div>
const removeAgent = async (id: string) => {
const updated = agents.filter((a) => a.id !== id);
const result = await deleteAgentAction({ agentId: id, organizationId: orgId });
if (result?.data?.success === false) {
toast.error("Failed to delete agent");
} else {
await updateContext({ flowData: { ...state?.context.flowData, agents: updated } });
}
};
const onContinue = async () => {
await next();
};
return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold">Create an agent</h1>
<p className="text-sm text-muted-foreground mt-1">
Optional agents will use your default notifier and storage.
</p>
{agents.length > 0 && (
<div className="flex flex-col gap-1">
{agents.map((agent) => (
<div
key={agent.id}
className="flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 p-2 text-sm text-primary"
>
<span className="flex-1 truncate">{agent.name}</span>
<button
type="button"
onClick={() => deleteAgent.mutate(agent.id)}
disabled={deleteAgent.isPending}
className="opacity-50 hover:opacity-100 transition-opacity"
>
<X className="size-4" />
</button>
</div>
{agents.length > 0 && (
<div className="flex flex-col gap-1">
{agents.map((agent) => (
<div
key={agent.id}
className="flex items-center gap-2 rounded-lg border border-primary/20 bg-primary/10 p-2 text-sm text-primary"
>
<span className="flex-1 truncate">{agent.name}</span>
<button
type="button"
onClick={() => removeAgent(agent.id)}
className="opacity-50 hover:opacity-100 transition-opacity"
>
<X className="size-4" />
</button>
</div>
))}
</div>
)}
<div className="flex gap-2">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="agent-prod"
onKeyDown={(e) => e.key === "Enter" && addAgent()}
disabled={adding}
/>
<Button
type="button"
variant="outline"
onClick={addAgent}
disabled={adding || !name.trim()}
>
{adding ? "Adding…" : "Add"}
</Button>
</div>
<Button type="button" onClick={onContinue}>
Continue
</Button>
))}
</div>
);
)}
<div className="flex gap-2">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="agent-prod"
onKeyDown={(e) => e.key === "Enter" && onAdd()}
disabled={createAgent.isPending}
/>
<Button
type="button"
variant="outline"
onClick={onAdd}
disabled={createAgent.isPending || !name.trim()}
>
{createAgent.isPending ? "Adding…" : "Add"}
</Button>
</div>
<Button type="button" onClick={() => next()}>
Continue
</Button>
</div>
);
};