updates
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState } from "react"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { Settings2, Plus, ArrowLeft } from "lucide-react"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import { DataTable } from "@/shared/data-view/table-view"
|
||||
import { createActionsColumn } from "@/shared/data-view/table-view"
|
||||
import { useCrudDialog, type CrudDialogClient, type UseCrudDialogOptions } from "./use-crud-dialog"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type CrudDialogProps<TClient extends CrudDialogClient> = UseCrudDialogOptions<TClient> & {
|
||||
/** Dialog title shown in the header */
|
||||
title: string
|
||||
/** Table columns (receives `openEdit` and `handleDelete` helpers) */
|
||||
columns: (helpers: {
|
||||
openEdit: (row: any) => void
|
||||
handleDelete: (row: any) => Promise<void>
|
||||
}) => ColumnDef<any>[]
|
||||
/** Render the create/edit form */
|
||||
renderForm: (props: {
|
||||
resourceId: string | null
|
||||
initialData: any
|
||||
onSuccess: () => void
|
||||
}) => React.ReactNode
|
||||
/** Optional trigger button; defaults to a settings icon button */
|
||||
trigger?: React.ReactNode
|
||||
/** Custom trigger class */
|
||||
triggerClassName?: string
|
||||
}
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function CrudDialog<TClient extends CrudDialogClient>({
|
||||
title,
|
||||
columns: columnsFn,
|
||||
renderForm,
|
||||
trigger,
|
||||
triggerClassName,
|
||||
...hookOptions
|
||||
}: CrudDialogProps<TClient>) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const crud = useCrudDialog(hookOptions)
|
||||
|
||||
const columns = columnsFn({
|
||||
openEdit: crud.openEdit,
|
||||
handleDelete: crud.handleDelete,
|
||||
})
|
||||
|
||||
// Add actions column
|
||||
const allColumns: ColumnDef<any>[] = [
|
||||
...columns,
|
||||
createActionsColumn({
|
||||
onEdit: crud.openEdit,
|
||||
onDelete: crud.handleDelete,
|
||||
}),
|
||||
]
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
crud.closeForm()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{trigger ? (
|
||||
<div onClick={() => setIsOpen(true)}>{trigger}</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={triggerClassName ?? "h-5 w-5"}
|
||||
onClick={() => setIsOpen(true)}
|
||||
title={`Manage ${title}`}
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={(v) => { if (!v) handleClose() }}>
|
||||
<DialogContent className="min-w-2xl max-w-3xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{crud.isFormOpen && (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={crud.closeForm}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
{crud.isFormOpen
|
||||
? (crud.editingId ? `Edit ${title}` : `New ${title}`)
|
||||
: title}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[75vh]">
|
||||
{crud.isFormOpen ? (
|
||||
<div className="px-1">
|
||||
{renderForm({
|
||||
resourceId: crud.editingId,
|
||||
initialData: crud.editingItem,
|
||||
onSuccess: crud.handleFormSuccess,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 px-1">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={crud.openCreate}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add {title}
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={allColumns}
|
||||
data={crud.items}
|
||||
pagination={crud.pagination}
|
||||
sorting={crud.sorting}
|
||||
onChange={crud.handleChange}
|
||||
isLoading={crud.isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CrudDialog, type CrudDialogProps } from "./crud-dialog"
|
||||
export { useCrudDialog, type CrudDialogClient, type UseCrudDialogOptions } from "./use-crud-dialog"
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { confirm } from "@/shared/components/confirm-dialog"
|
||||
import type { DataViewChangeEvent, DataViewPaginationState, DataViewSorting } from "@/shared/data-view/table-view"
|
||||
|
||||
// ── Types ──
|
||||
|
||||
export type CrudDialogClient = {
|
||||
list(query?: any): Promise<any>
|
||||
create(payload: any): Promise<any>
|
||||
update(id: string, payload: any): Promise<any>
|
||||
destroy(id: string): Promise<any>
|
||||
}
|
||||
|
||||
export type UseCrudDialogOptions<TClient extends CrudDialogClient> = {
|
||||
queryKey: string[]
|
||||
getClient: () => TClient
|
||||
resourceLabel?: string
|
||||
}
|
||||
|
||||
// ── Hook ──
|
||||
|
||||
export function useCrudDialog<TClient extends CrudDialogClient>({
|
||||
queryKey,
|
||||
getClient,
|
||||
resourceLabel = "item",
|
||||
}: UseCrudDialogOptions<TClient>) {
|
||||
const client = getClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// ── Local pagination state (no URL pollution) ──
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [sortBy, setSortBy] = useState<string | null>(null)
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null)
|
||||
|
||||
// ── Form dialog state ──
|
||||
const [isFormOpen, setIsFormOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editingItem, setEditingItem] = useState<any>(null)
|
||||
|
||||
const fullQueryKey = [...queryKey, { page, pageSize, sortBy, sortOrder }]
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: fullQueryKey,
|
||||
queryFn: () => {
|
||||
const params: Record<string, unknown> = { page, per_page: pageSize }
|
||||
if (sortBy) params.sort_by = sortBy
|
||||
if (sortOrder) params.sort_order = sortOrder
|
||||
return client.list(params)
|
||||
},
|
||||
})
|
||||
|
||||
const responseData = (data as any)?.data ?? []
|
||||
const items = Array.isArray(responseData) ? responseData : []
|
||||
const meta = (data as any)?.meta
|
||||
|
||||
const pagination: DataViewPaginationState = {
|
||||
page,
|
||||
pageSize,
|
||||
pageCount: meta?.last_page ?? 1,
|
||||
total: meta?.total ?? 0,
|
||||
}
|
||||
|
||||
const sorting: DataViewSorting = sortBy
|
||||
? [{ id: sortBy, desc: sortOrder === "desc" }]
|
||||
: []
|
||||
|
||||
const handleChange = (event: DataViewChangeEvent) => {
|
||||
switch (event.type) {
|
||||
case "pagination":
|
||||
setPage(event.pagination.page)
|
||||
setPageSize(event.pagination.pageSize)
|
||||
break
|
||||
case "sorting": {
|
||||
const sort = event.sorting[0]
|
||||
setSortBy(sort?.id ?? null)
|
||||
setSortOrder(sort ? (sort.desc ? "desc" : "asc") : null)
|
||||
setPage(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invalidateQuery = () => {
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
}
|
||||
|
||||
const { mutateAsync: deleteItem } = useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
const promise = client.destroy(id)
|
||||
toast.promise(promise, {
|
||||
loading: `Deleting ${resourceLabel}...`,
|
||||
success: `${resourceLabel} deleted`,
|
||||
error: `Failed to delete ${resourceLabel}`,
|
||||
})
|
||||
return promise
|
||||
},
|
||||
onSuccess: invalidateQuery,
|
||||
})
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingId(null)
|
||||
setEditingItem(null)
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (row: any) => {
|
||||
setEditingId(String(row.id))
|
||||
setEditingItem(row)
|
||||
setIsFormOpen(true)
|
||||
}
|
||||
|
||||
const closeForm = () => {
|
||||
setIsFormOpen(false)
|
||||
setEditingId(null)
|
||||
setEditingItem(null)
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
const confirmed = await confirm({
|
||||
title: `Delete this ${resourceLabel}?`,
|
||||
description: "This action cannot be undone.",
|
||||
confirmLabel: "Delete",
|
||||
variant: "destructive",
|
||||
})
|
||||
if (confirmed) await deleteItem(String(row.id))
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
invalidateQuery()
|
||||
closeForm()
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
pagination,
|
||||
sorting,
|
||||
handleChange,
|
||||
isFormOpen,
|
||||
editingId,
|
||||
editingItem,
|
||||
openCreate,
|
||||
openEdit,
|
||||
closeForm,
|
||||
handleDelete,
|
||||
handleFormSuccess,
|
||||
invalidateQuery,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user