Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
71 lines
3.9 KiB
Markdown
71 lines
3.9 KiB
Markdown
# Admin dashboard data caching (TanStack Query)
|
|
|
|
The admin dashboard (`app/(dashboard)/**`) is being migrated to TanStack Query
|
|
v5, the same library already used by the consumer app (see
|
|
`docs/consumer-caching.md`). Read that doc first for the shared basics
|
|
(`lib/queryClient.ts`, `QueryClientProvider` in `app/providers.tsx`,
|
|
`unwrapService`). This doc only covers where the admin side intentionally
|
|
differs.
|
|
|
|
## `queries/admin/adminKeys.ts` is the admin key factory
|
|
|
|
Parallel to `queries/consumerKeys.ts`, scoped under `['admin', ...]` instead
|
|
of `['consumer', ...]`. One hook file per data domain lives in
|
|
`queries/admin/*.ts`, same shape as `queries/consumer/*.ts`.
|
|
|
|
## `PaginatedList` owns one `useQuery` internally -- most admin pages need no changes
|
|
|
|
17 of the 20 admin sections render data through `components/PaginatedList.tsx`,
|
|
passing only `url`/`itemsKey`/`columns`/`urlParams`. That component now runs
|
|
a single `useQuery` internally, keyed on `adminKeys.list(url, resolvedParams)`
|
|
where `resolvedParams` is the exact `{page, pageSize, sort, filters, ...}`
|
|
object sent to the server (from `buildListParams`). Callers don't need their
|
|
own query hook for the list itself -- just keep passing `url`/`columns` as
|
|
before. `refresh()` on the `PaginatedListHandle` ref now does
|
|
`queryClient.invalidateQueries({ queryKey: adminKeys.listByUrl(url) })`
|
|
instead of a local re-render counter. The imperative method remains available
|
|
for non-row workflows, while row actions use `useAdminMutation` directly as
|
|
described below.
|
|
|
|
**Why `useQuery`, not `useInfiniteQuery`:** unlike every consumer list
|
|
(`consumer-caching.md`'s "always infinite query" rule), `PaginatedList` is a
|
|
page-jump table -- `page`, `pageSize`, `totalItemsCount`, not an accumulating
|
|
feed. `useInfiniteQuery` doesn't fit that shape. `placeholderData:
|
|
keepPreviousData` is used instead, so changing page/sort/filter shows the
|
|
previous page's rows (not a blank table) while the new page loads.
|
|
|
|
**Why the fetch itself doesn't need a `services/*`/`ServiceResult<T>`
|
|
wrapper:** `components/paginated-list/paginatedListApi.ts`'s
|
|
`fetchPaginatedList` already calls `axiosInstance` directly and either
|
|
resolves `{ items, pagination }` or throws -- exactly what a bare `queryFn`
|
|
wants. Only genuine cancellation (`CanceledError`/`AbortError`, from
|
|
`useQuery`'s own signal) is rethrown as-is so TanStack's built-in
|
|
cancellation handling still applies; every other error is normalized through
|
|
`handleServiceError(error, { errorMode: 'silent' })` before being thrown, so
|
|
`query.error.message` still gives the same localized text the old
|
|
`loadError` state used to.
|
|
|
|
## `hooks/useAdminMutation.ts` owns admin row mutations and list invalidation
|
|
|
|
The identity-verifications, reviews, settlements, user-reports, and
|
|
withdrawal-requests sections use one mutation shape. The hook preserves the
|
|
existing `runAction(id, action, successMessage)` call-site and exposes a
|
|
per-row `pendingId`, while its successful mutation callback invalidates
|
|
`adminKeys.listByUrl(url)`. Pages therefore do not need to thread a
|
|
`PaginatedListHandle` ref through action callbacks just to refresh the list.
|
|
|
|
`runAction` resolves to `true` on success and `false` after a handled failure.
|
|
Modal actions use that result to close/reset the modal only after the server
|
|
accepts the mutation. Axios/service errors are normalized in the hook and
|
|
shown once through the shared toast copy.
|
|
|
|
## Raw-`axiosInstance` mutations don't need a service-layer rewrite first
|
|
|
|
Most non-CRUD admin actions (approve/reject/hide/restore/etc.) call
|
|
`axiosInstance` directly with no `services/*.ts` wrapper. Axios already
|
|
throws on non-2xx, which is exactly what a bare `mutationFn` wants -- there's
|
|
no need to introduce a `ServiceResult<T>` service function before writing the
|
|
mutation hook. Reserve `unwrapService(await X(..., { errorMode: 'parent' }))`
|
|
for the sections that already have a real service (tags, blog-articles,
|
|
event-categories, admin user detail/edit, wallet credit, geography).
|