← full-stack-fastapi-template  /  frontend/src/components/Items/EditItem.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation, useQueryClient } from "@tanstack/react-query"
3
import { Pencil } from "lucide-react"
4
import { useState } from "react"
5
import { useForm } from "react-hook-form"
6
import { z } from "zod"
7
8
import { type ItemPublic, ItemsService } from "@/client"
9
import { Button } from "@/components/ui/button"
10
import {
11
  Dialog,
12
  DialogClose,
13
  DialogContent,
14
  DialogDescription,
15
  DialogFooter,
16
  DialogHeader,
17
  DialogTitle,
18
} from "@/components/ui/dialog"
19
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
20
import {
21
  Form,
22
  FormControl,
23
  FormField,
24
  FormItem,
25
  FormLabel,
26
  FormMessage,
27
} from "@/components/ui/form"
28
import { Input } from "@/components/ui/input"
29
import { LoadingButton } from "@/components/ui/loading-button"
30
import useCustomToast from "@/hooks/useCustomToast"
31
import { handleError } from "@/utils"
32
33
const formSchema = z.object({
34
  title: z.string().min(1, { message: "Title is required" }),
35
  description: z.string().optional(),
36
})
37
38
type FormData = z.infer<typeof formSchema>
39
40
interface EditItemProps {
41
  item: ItemPublic
42
  onSuccess: () => void
43
}
44
45
const EditItem = ({ item, onSuccess }: EditItemProps) => {
46
  const [isOpen, setIsOpen] = useState(false)
47
  const queryClient = useQueryClient()
48
  const { showSuccessToast, showErrorToast } = useCustomToast()
49
50
  const form = useForm<FormData>({
51
    resolver: zodResolver(formSchema),
52
    mode: "onBlur",
53
    criteriaMode: "all",
54
    defaultValues: {
55
      title: item.title,
56
      description: item.description ?? undefined,
57
    },
58
  })
59
60
  const mutation = useMutation({
61
    mutationFn: (data: FormData) =>
62
      ItemsService.updateItem({ id: item.id, requestBody: data }),
63
    onSuccess: () => {
64
      showSuccessToast("Item updated successfully")
65
      setIsOpen(false)
66
      onSuccess()
67
    },
68
    onError: handleError.bind(showErrorToast),
69
    onSettled: () => {
70
      queryClient.invalidateQueries({ queryKey: ["items"] })
71
    },
72
  })
73
74
  const onSubmit = (data: FormData) => {
75
    mutation.mutate(data)
76
  }
77
78
  return (
79
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
80
      <DropdownMenuItem
81
        onSelect={(e) => e.preventDefault()}
82
        onClick={() => setIsOpen(true)}
83
      >
84
        <Pencil />
85
        Edit Item
86
      </DropdownMenuItem>
87
      <DialogContent className="sm:max-w-md">
88
        <Form {...form}>
89
          <form onSubmit={form.handleSubmit(onSubmit)}>
90
            <DialogHeader>
91
              <DialogTitle>Edit Item</DialogTitle>
92
              <DialogDescription>
93
                Update the item details below.
94
              </DialogDescription>
95
            </DialogHeader>
96
            <div className="grid gap-4 py-4">
97
              <FormField
98
                control={form.control}
99
                name="title"
100
                render={({ field }) => (
101
                  <FormItem>
102
                    <FormLabel>
103
                      Title <span className="text-destructive">*</span>
104
                    </FormLabel>
105
                    <FormControl>
106
                      <Input placeholder="Title" type="text" {...field} />
107
                    </FormControl>
108
                    <FormMessage />
109
                  </FormItem>
110
                )}
111
              />
112
113
              <FormField
114
                control={form.control}
115
                name="description"
116
                render={({ field }) => (
117
                  <FormItem>
118
                    <FormLabel>Description</FormLabel>
119
                    <FormControl>
120
                      <Input placeholder="Description" type="text" {...field} />
121
                    </FormControl>
122
                    <FormMessage />
123
                  </FormItem>
124
                )}
125
              />
126
            </div>
127
128
            <DialogFooter>
129
              <DialogClose asChild>
130
                <Button variant="outline" disabled={mutation.isPending}>
131
                  Cancel
132
                </Button>
133
              </DialogClose>
134
              <LoadingButton type="submit" loading={mutation.isPending}>
135
                Save
136
              </LoadingButton>
137
            </DialogFooter>
138
          </form>
139
        </Form>
140
      </DialogContent>
141
    </Dialog>
142
  )
143
}
144
145
export default EditItem
146