Remove item management feature from frontend and backend, and add a pending skeleton component.
All checks were successful
Deploy to Production / deploy (push) Successful in 1m34s

This commit is contained in:
魏风
2026-03-13 11:46:15 +08:00
parent ef93d4e5c2
commit 3c9a0343e9
19 changed files with 9 additions and 1069 deletions

View File

@@ -1,144 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Plus } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { type ItemCreate, ItemsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})
type FormData = z.infer<typeof formSchema>
const AddItem = () => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
title: "",
description: "",
},
})
const mutation = useMutation({
mutationFn: (data: ItemCreate) =>
ItemsService.createItem({ requestBody: data }),
onSuccess: () => {
showSuccessToast("Item created successfully")
form.reset()
setIsOpen(false)
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})
const onSubmit = (data: FormData) => {
mutation.mutate(data)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="my-4">
<Plus className="mr-2" />
Add Item
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add Item</DialogTitle>
<DialogDescription>
Fill in the details to add a new item.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Title <span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input
placeholder="Title"
type="text"
{...field}
required
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton type="submit" loading={mutation.isPending}>
Save
</LoadingButton>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
export default AddItem

View File

@@ -1,94 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Trash2 } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { ItemsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
interface DeleteItemProps {
id: string
onSuccess: () => void
}
const DeleteItem = ({ id, onSuccess }: DeleteItemProps) => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const { handleSubmit } = useForm()
const deleteItem = async (id: string) => {
await ItemsService.deleteItem({ id: id })
}
const mutation = useMutation({
mutationFn: deleteItem,
onSuccess: () => {
showSuccessToast("The item was deleted successfully")
setIsOpen(false)
onSuccess()
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries()
},
})
const onSubmit = async () => {
mutation.mutate(id)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuItem
variant="destructive"
onSelect={(e) => e.preventDefault()}
onClick={() => setIsOpen(true)}
>
<Trash2 />
Delete Item
</DropdownMenuItem>
<DialogContent className="sm:max-w-md">
<form onSubmit={handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Delete Item</DialogTitle>
<DialogDescription>
This item will be permanently deleted. Are you sure? You will not
be able to undo this action.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton
variant="destructive"
type="submit"
loading={mutation.isPending}
>
Delete
</LoadingButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export default DeleteItem

View File

@@ -1,145 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { Pencil } from "lucide-react"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { type ItemPublic, ItemsService } from "@/client"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { LoadingButton } from "@/components/ui/loading-button"
import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
const formSchema = z.object({
title: z.string().min(1, { message: "Title is required" }),
description: z.string().optional(),
})
type FormData = z.infer<typeof formSchema>
interface EditItemProps {
item: ItemPublic
onSuccess: () => void
}
const EditItem = ({ item, onSuccess }: EditItemProps) => {
const [isOpen, setIsOpen] = useState(false)
const queryClient = useQueryClient()
const { showSuccessToast, showErrorToast } = useCustomToast()
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: "onBlur",
criteriaMode: "all",
defaultValues: {
title: item.title,
description: item.description ?? undefined,
},
})
const mutation = useMutation({
mutationFn: (data: FormData) =>
ItemsService.updateItem({ id: item.id, requestBody: data }),
onSuccess: () => {
showSuccessToast("Item updated successfully")
setIsOpen(false)
onSuccess()
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})
const onSubmit = (data: FormData) => {
mutation.mutate(data)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
onClick={() => setIsOpen(true)}
>
<Pencil />
Edit Item
</DropdownMenuItem>
<DialogContent className="sm:max-w-md">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Edit Item</DialogTitle>
<DialogDescription>
Update the item details below.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>
Title <span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input placeholder="Title" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="Description" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" disabled={mutation.isPending}>
Cancel
</Button>
</DialogClose>
<LoadingButton type="submit" loading={mutation.isPending}>
Save
</LoadingButton>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
export default EditItem

View File

@@ -1,34 +0,0 @@
import { EllipsisVertical } from "lucide-react"
import { useState } from "react"
import type { ItemPublic } from "@/client"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import DeleteItem from "../Items/DeleteItem"
import EditItem from "../Items/EditItem"
interface ItemActionsMenuProps {
item: ItemPublic
}
export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<EditItem item={item} onSuccess={() => setOpen(false)} />
<DeleteItem id={item.id} onSuccess={() => setOpen(false)} />
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -1,73 +0,0 @@
import type { ColumnDef } from "@tanstack/react-table"
import { Check, Copy } from "lucide-react"
import type { ItemPublic } from "@/client"
import { Button } from "@/components/ui/button"
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
import { cn } from "@/lib/utils"
import { ItemActionsMenu } from "./ItemActionsMenu"
function CopyId({ id }: { id: string }) {
const [copiedText, copy] = useCopyToClipboard()
const isCopied = copiedText === id
return (
<div className="flex items-center gap-1.5 group">
<span className="font-mono text-xs text-muted-foreground">{id}</span>
<Button
variant="ghost"
size="icon"
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copy(id)}
>
{isCopied ? (
<Check className="size-3 text-green-500" />
) : (
<Copy className="size-3" />
)}
<span className="sr-only">Copy ID</span>
</Button>
</div>
)
}
export const columns: ColumnDef<ItemPublic>[] = [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => <CopyId id={row.original.id} />,
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => (
<span className="font-medium">{row.original.title}</span>
),
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => {
const description = row.original.description
return (
<span
className={cn(
"max-w-xs truncate block text-muted-foreground",
!description && "italic",
)}
>
{description || "No description"}
</span>
)
},
},
{
id: "actions",
header: () => <span className="sr-only">Actions</span>,
cell: ({ row }) => (
<div className="flex justify-end">
<ItemActionsMenu item={row.original} />
</div>
),
},
]

View File

@@ -8,7 +8,7 @@ import {
TableRow,
} from "@/components/ui/table"
const PendingItems = () => (
const PendingSkeleton = () => (
<Table>
<TableHeader>
<TableRow>
@@ -43,4 +43,4 @@ const PendingItems = () => (
</Table>
)
export default PendingItems
export default PendingSkeleton

View File

@@ -1,4 +1,4 @@
import { Briefcase, Home, MapPin, Users } from "lucide-react"
import { Home, MapPin, Users } from "lucide-react"
import { SidebarAppearance } from "@/components/Common/Appearance"
import { Logo } from "@/components/Common/Logo"
@@ -15,7 +15,6 @@ import { User } from "./User"
const baseItems: Item[] = [
{ icon: Home, title: "Dashboard", path: "/" },
{ icon: MapPin, title: "Locations", path: "/locations" },
{ icon: Briefcase, title: "Items", path: "/items" },
]
export function AppSidebar() {