refactor: migrate tickets and users pages to use new dashboard list components

- Refactored TicketsPage to utilize DashboardListPage for improved filtering and data fetching.
- Removed unnecessary state management and loading logic in favor of the new list hooks.
- Updated user management page to leverage useDashboardPagedList for cleaner pagination and filtering.
- Enhanced CRUD page to support new action state management and toolbar customization.
- Added reset filters functionality to dashboard list components.
- Updated translations for new UI elements and loading states.
This commit is contained in:
mlogclub
2026-05-28 12:38:33 +08:00
parent 0611fb0bb6
commit 3fe5a89aeb
14 changed files with 1060 additions and 1355 deletions
@@ -1,7 +1,7 @@
"use client"
import type { ReactNode } from "react"
import { useState } from "react"
import { useCallback, useEffect, useState } from "react"
import {
closestCenter,
DndContext,
@@ -111,6 +111,12 @@ export type DashboardCrudRowAction<TItem> = DashboardCrudActionRule<TItem> & {
run: (context: DashboardCrudRowActionContext<TItem>) => Promise<void> | void
}
export type DashboardCrudActionState = {
onRefresh: () => void
onCreate: () => void
loading: boolean
}
export type DashboardCrudPageProps<TItem, TPayload> = {
filters: DashboardCrudFilter[]
columns: DashboardCrudColumn<TItem>[]
@@ -142,6 +148,7 @@ export type DashboardCrudPageProps<TItem, TPayload> = {
getItemId: (item: TItem) => number
createItem: (payload: TPayload) => Promise<unknown>
updateItem: (item: TItem, payload: TPayload) => Promise<unknown>
canEdit?: (item: TItem) => boolean
deleteItem?: (item: TItem) => Promise<unknown>
canDelete?: (item: TItem) => boolean
deleteConfirm?: false | ConfirmOptions | ((item: TItem) => ConfirmOptions)
@@ -156,6 +163,11 @@ export type DashboardCrudPageProps<TItem, TPayload> = {
handleLabel: string
}
pageSize?: number
layout?: "page" | "fragment"
showToolbar?: boolean
showToolbarActions?: boolean
tableShellClassName?: string
onActionStateChange?: (state: DashboardCrudActionState) => void
labels: {
refresh: string
create: string
@@ -185,6 +197,7 @@ export function DashboardCrudPage<TItem, TPayload>({
getItemId,
createItem,
updateItem,
canEdit,
deleteItem,
canDelete,
deleteConfirm,
@@ -192,6 +205,11 @@ export function DashboardCrudPage<TItem, TPayload>({
renderRowActions,
sort,
pageSize = 20,
layout = "page",
showToolbar = true,
showToolbarActions = true,
tableShellClassName,
onActionStateChange,
labels,
}: DashboardCrudPageProps<TItem, TPayload>) {
const confirm = useConfirm()
@@ -231,16 +249,24 @@ export function DashboardCrudPage<TItem, TPayload>({
handleApplyFilters()
}
function openCreateDialog() {
const openCreateDialog = useCallback(() => {
setEditingItem(null)
setDialogOpen(true)
}
}, [])
function openEditDialog(item: TItem) {
setEditingItem(item)
setDialogOpen(true)
}
useEffect(() => {
onActionStateChange?.({
onRefresh: () => void loadData(),
onCreate: openCreateDialog,
loading,
})
}, [loadData, loading, onActionStateChange, openCreateDialog])
function handleDialogOpenChange(open: boolean) {
if (saving) return
if (!open) setEditingItem(null)
@@ -379,7 +405,12 @@ export function DashboardCrudPage<TItem, TPayload>({
))}
<TableCell className="text-right">
<ButtonGroup className="ml-auto">
<Button variant="outline" size="sm" onClick={() => openEditDialog(item)}>
<Button
variant="outline"
size="sm"
disabled={canEdit ? !canEdit(item) : false}
onClick={() => openEditDialog(item)}
>
{labels.edit}
</Button>
{renderRowActions || rowActions.length > 0 || deleteItem ? (
@@ -475,21 +506,23 @@ export function DashboardCrudPage<TItem, TPayload>({
)
}
return (
const content = (
<>
<DashboardPage>
{showToolbar ? (
<DashboardToolbar
actions={
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : undefined} />
{labels.refresh}
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
{labels.create}
</Button>
</>
showToolbarActions ? (
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : undefined} />
{labels.refresh}
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
{labels.create}
</Button>
</>
) : null
}
>
{filters.map((filter) => {
@@ -525,86 +558,104 @@ export function DashboardCrudPage<TItem, TPayload>({
{labels.query}
</Button>
</DashboardToolbar>
) : null}
<DashboardTableShell
pagination={
<ListPagination
page={result.page.page}
total={result.page.total}
limit={list.limit}
loading={loading}
onPageChange={list.handlePageChange}
onLimitChange={list.handleLimitChange}
/>
}
>
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
{sortable ? <TableHead className="w-10" /> : null}
{columns.map((column) => (
<TableHead key={column.key} className={column.className}>
{column.label}
</TableHead>
))}
<TableHead className="w-[92px] text-right">
{labels.actions}
<DashboardTableShell
className={tableShellClassName}
pagination={
<ListPagination
page={result.page.page}
total={result.page.total}
limit={list.limit}
loading={loading}
onPageChange={list.handlePageChange}
onLimitChange={list.handleLimitChange}
/>
}
>
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
{sortable ? <TableHead className="w-10" /> : null}
{columns.map((column) => (
<TableHead key={column.key} className={column.className}>
{column.label}
</TableHead>
</TableRow>
</TableHeader>
{sortable ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(event) => void handleSortEnd(event)}
))}
<TableHead className="w-[92px] text-right">
{labels.actions}
</TableHead>
</TableRow>
</TableHeader>
{sortable ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(event) => void handleSortEnd(event)}
>
<SortableContext
items={result.results.map((item) => String(getItemId(item)))}
strategy={verticalListSortingStrategy}
>
<SortableContext
items={result.results.map((item) => String(getItemId(item)))}
strategy={verticalListSortingStrategy}
>
<TableBody>
{result.results.map((item) =>
renderTableRow(item, {
sortable: true,
sortDisabled: sort?.disabled,
})
)}
{renderStateRow()}
</TableBody>
</SortableContext>
</DndContext>
) : (
<TableBody>
{result.results.map((item) => renderTableRow(item))}
{renderStateRow()}
</TableBody>
)}
</Table>
</DashboardTableShell>
</DashboardPage>
{form ? (
<DashboardCrudFormDialog
open={dialogOpen}
saving={saving}
item={editingItem}
itemId={editingItem ? getItemId(editingItem) : null}
fields={form.fields}
fetchDetail={form.fetchDetail}
transformSubmitValues={form.transformSubmitValues}
labels={form.labels}
onOpenChange={handleDialogOpenChange}
onSubmit={handleSubmit}
/>
) : (
renderEditDialog?.({
open: dialogOpen,
saving,
item: editingItem,
itemId: editingItem ? getItemId(editingItem) : null,
onOpenChange: handleDialogOpenChange,
onSubmit: handleSubmit,
})
)}
<TableBody>
{result.results.map((item) =>
renderTableRow(item, {
sortable: true,
sortDisabled: sort?.disabled,
})
)}
{renderStateRow()}
</TableBody>
</SortableContext>
</DndContext>
) : (
<TableBody>
{result.results.map((item) => renderTableRow(item))}
{renderStateRow()}
</TableBody>
)}
</Table>
</DashboardTableShell>
</>
)
const dialog = form ? (
<DashboardCrudFormDialog
open={dialogOpen}
saving={saving}
item={editingItem}
itemId={editingItem ? getItemId(editingItem) : null}
fields={form.fields}
fetchDetail={form.fetchDetail}
transformSubmitValues={form.transformSubmitValues}
labels={form.labels}
onOpenChange={handleDialogOpenChange}
onSubmit={handleSubmit}
/>
) : (
renderEditDialog?.({
open: dialogOpen,
saving,
item: editingItem,
itemId: editingItem ? getItemId(editingItem) : null,
onOpenChange: handleDialogOpenChange,
onSubmit: handleSubmit,
})
)
if (layout === "fragment") {
return (
<>
{content}
{dialog}
</>
)
}
return (
<>
<DashboardPage>{content}</DashboardPage>
{dialog}
</>
)
}
+1
View File
@@ -11,6 +11,7 @@ export {
export { useDashboardCrudFilters } from "./use-dashboard-crud-filters"
export type {
DashboardCrudColumn,
DashboardCrudActionState,
DashboardCrudDialogProps,
DashboardCrudFilter,
DashboardCrudPageProps,
@@ -48,6 +48,11 @@ export function useDashboardCrudFilters(
}))
}
function resetFilters() {
setDraftFilters(initialFilters)
setAppliedFilters(initialFilters)
}
return {
draftFilters,
appliedFilters,
@@ -55,5 +60,6 @@ export function useDashboardCrudFilters(
setDraftFilters,
applyFilter,
applyFilters,
resetFilters,
}
}
@@ -54,6 +54,7 @@ export type DashboardListRenderContext<TItem> = {
result: DashboardCrudPageResult<TItem>
loading: boolean
reload: () => Promise<void>
resetFilters: () => void
}
export type DashboardListPageProps<TItem> = {
@@ -104,6 +105,7 @@ export function DashboardListPage<TItem>({
result: list.result,
loading: list.loading,
reload: list.loadData,
resetFilters: list.resetFilters,
}
function handleFilterKeyDown(event: KeyboardEvent<HTMLInputElement>) {
+4 -1
View File
@@ -6,4 +6,7 @@ export type {
DashboardListPageProps,
DashboardListRenderContext,
} from "./dashboard-list-page"
export type { DashboardPagedListOptions } from "./use-dashboard-paged-list"
export type {
DashboardPagedListFilter,
DashboardPagedListOptions,
} from "./use-dashboard-paged-list"
@@ -39,7 +39,14 @@ export function useDashboardPagedList<TItem>({
`${filter.name}:${String(filter.defaultValue)}:${String(filter.allValue)}:${filter.trim ? "1" : "0"}:${filter.valueType ?? ""}`
)
.join("|")
const { draftFilters, appliedFilters, setDraftFilter, applyFilter, applyFilters } =
const {
draftFilters,
appliedFilters,
setDraftFilter,
applyFilter,
applyFilters,
resetFilters,
} =
useDashboardCrudFilters(filters)
const [page, setPage] = useState(1)
const [limit, setLimit] = useState(pageSize)
@@ -100,6 +107,11 @@ export function useDashboardPagedList<TItem>({
setPage(1)
}
function resetDraftFilters() {
resetFilters()
setPage(1)
}
return {
draftFilters,
setDraftFilter,
@@ -115,5 +127,6 @@ export function useDashboardPagedList<TItem>({
loadData,
handlePageChange,
handleLimitChange,
resetFilters: resetDraftFilters,
}
}