import { useMemo, useState } from "react" import { actionsApi } from "@/api/actions.ts" import type { DtoActionResponse } from "@/sdk" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { AlertCircle, CircleSlash2, Loader2, Pencil, Plus, Search, Trash2 } from "lucide-react" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { Badge } from "@/components/ui/badge.tsx" import { Button } from "@/components/ui/button.tsx" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog.tsx" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx" import { Input } from "@/components/ui/input.tsx" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx" import { Textarea } from "@/components/ui/textarea.tsx" const createActionSchema = z.object({ label: z.string().trim().min(1, "Label is required").max(255, "Max 255 chars"), description: z.string().trim().min(1, "Description is required"), make_target: z .string() .trim() .min(1, "Make target is required") .max(255, "Max 255 chars") // keep client-side fairly strict to avoid surprises; server should also validate .regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/, "Invalid make target (allowed: a-z A-Z 0-9 . _ -)"), }) type CreateActionInput = z.input const updateActionSchema = createActionSchema.partial() type UpdateActionInput = z.input function TargetBadge({ target }: { target?: string | null }) { if (!target) { return ( ) } return ( {target} ) } export const ActionsPage = () => { const qc = useQueryClient() const [filter, setFilter] = useState("") const [createOpen, setCreateOpen] = useState(false) const [updateOpen, setUpdateOpen] = useState(false) const [deleteId, setDeleteId] = useState(null) const [editing, setEditing] = useState(null) const actionsQ = useQuery({ queryKey: ["admin-actions"], queryFn: () => actionsApi.listActions(), }) const filtered = useMemo(() => { const data: DtoActionResponse[] = actionsQ.data ?? [] const q = filter.trim().toLowerCase() if (!q) return data return data.filter((a) => { return ( (a.label ?? "").toLowerCase().includes(q) || (a.description ?? "").toLowerCase().includes(q) || (a.make_target ?? "").toLowerCase().includes(q) ) }) }, [filter, actionsQ.data]) const createForm = useForm({ resolver: zodResolver(createActionSchema), defaultValues: { label: "", description: "", make_target: "", }, }) const createMut = useMutation({ mutationFn: (values: CreateActionInput) => actionsApi.createAction(values), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["admin-actions"] }) createForm.reset() setCreateOpen(false) toast.success("Action created.") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to create action.") }, }) const updateForm = useForm({ resolver: zodResolver(updateActionSchema), defaultValues: {}, }) const updateMut = useMutation({ mutationFn: ({ id, values }: { id: string; values: UpdateActionInput }) => actionsApi.updateAction(id, values), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["admin-actions"] }) updateForm.reset() setUpdateOpen(false) setEditing(null) toast.success("Action updated.") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to update action.") }, }) const openEdit = (a: DtoActionResponse) => { if (!a.id) return setEditing(a) updateForm.reset({ label: a.label ?? "", description: a.description ?? "", make_target: a.make_target ?? "", }) setUpdateOpen(true) } const deleteMut = useMutation({ mutationFn: (id: string) => actionsApi.deleteAction(id), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["admin-actions"] }) setDeleteId(null) toast.success("Action deleted.") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to delete action.") }, }) if (actionsQ.isLoading) return
Loading actions…
if (actionsQ.error) return
Error loading actions.
return (

Admin Actions

setFilter(e.target.value)} placeholder="Search actions" className="w-72 pl-8" />
Create Action
createMut.mutate(v))} > ( Label )} /> ( Make Target )} /> ( Description