diff --git a/apps/admin/public/assets/locales/en-US/invite.json b/apps/admin/public/assets/locales/en-US/invite.json index f7fcd9d..4ec7fe2 100644 --- a/apps/admin/public/assets/locales/en-US/invite.json +++ b/apps/admin/public/assets/locales/en-US/invite.json @@ -12,6 +12,8 @@ "inviter": "Inviter", "inviterGiftDays": "Inviter Gift Days", "inviterId": "Inviter ID", + "loadErrorDescription": "Refresh the table or try again later.", + "loadErrorTitle": "Failed to load invites", "no": "No", "orderCount": "Orders", "searchPlaceholder": "Email or phone", diff --git a/apps/admin/public/assets/locales/zh-CN/invite.json b/apps/admin/public/assets/locales/zh-CN/invite.json index 9f47028..5ff29f4 100644 --- a/apps/admin/public/assets/locales/zh-CN/invite.json +++ b/apps/admin/public/assets/locales/zh-CN/invite.json @@ -12,6 +12,8 @@ "inviter": "邀请人", "inviterGiftDays": "邀请人赠送天数", "inviterId": "邀请人 ID", + "loadErrorDescription": "刷新表格或稍后重试。", + "loadErrorTitle": "邀请记录加载失败", "no": "否", "orderCount": "订单数", "searchPlaceholder": "邮箱/手机号关键词", diff --git a/apps/admin/src/sections/invite/index.test.tsx b/apps/admin/src/sections/invite/index.test.tsx index a255767..c214e94 100644 --- a/apps/admin/src/sections/invite/index.test.tsx +++ b/apps/admin/src/sections/invite/index.test.tsx @@ -127,12 +127,33 @@ describe("InviteManagement", () => { expect(await screen.findByText("No invite records")).not.toBeNull(); }); - it("keeps the table empty when the request fails", async () => { + it("shows a loading state while invites are being fetched", () => { + let resolveRequest: + | (( + value: AxiosResponse< + API.Response & { data?: API.GetAdminInviteListResponse } + > + ) => void) + | undefined; + mockedGetAdminInviteList.mockReturnValue( + new Promise((resolve) => { + resolveRequest = resolve; + }) + ); + + render(); + + expect(screen.getByRole("status", { name: "Loading data" })).not.toBeNull(); + expect(resolveRequest).toBeTypeOf("function"); + }); + + it("shows an error state when the request fails", async () => { mockedGetAdminInviteList.mockRejectedValue(new Error("network failed")); render(); await waitFor(() => expect(mockedGetAdminInviteList).toHaveBeenCalled()); - expect(await screen.findByText("No invite records")).not.toBeNull(); + expect(await screen.findByText("Failed to load invites")).not.toBeNull(); + expect(screen.queryByText("No invite records")).toBeNull(); }); }); diff --git a/apps/admin/src/sections/invite/index.tsx b/apps/admin/src/sections/invite/index.tsx index b9a7736..3026247 100644 --- a/apps/admin/src/sections/invite/index.tsx +++ b/apps/admin/src/sections/invite/index.tsx @@ -1,5 +1,10 @@ "use client"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@workspace/ui/components/alert"; import { Avatar, AvatarFallback, @@ -12,6 +17,7 @@ import { type ProTableActions, } from "@workspace/ui/composed/pro-table/pro-table"; import { getAdminInviteList } from "@workspace/ui/services/admin/invite"; +import { CircleAlert } from "lucide-react"; import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { Display } from "@/components/display"; @@ -158,6 +164,17 @@ export default function InviteManagement() { }, ]} empty={} + error={ + + + } header={{ title: t("title", "Invite Management") }} params={[ { diff --git a/packages/ui/src/composed/pro-table/pro-table.tsx b/packages/ui/src/composed/pro-table/pro-table.tsx index 4a7124f..a111b09 100644 --- a/packages/ui/src/composed/pro-table/pro-table.tsx +++ b/packages/ui/src/composed/pro-table/pro-table.tsx @@ -39,7 +39,13 @@ import { SortableRow } from "@workspace/ui/composed/pro-table/sortable-row"; import { ProTableWrapper } from "@workspace/ui/composed/pro-table/wrapper"; import { cn } from "@workspace/ui/lib/utils"; import { useSize } from "ahooks"; -import { GripVertical, ListRestart, Loader, RefreshCcw } from "lucide-react"; +import { + CircleAlert, + GripVertical, + ListRestart, + Loader, + RefreshCcw, +} from "lucide-react"; import type React from "react"; import { Fragment, @@ -79,6 +85,7 @@ export interface ProTableProps { selectedRowsText: (total: number) => string; }>; empty?: React.ReactNode; + error?: React.ReactNode; onSort?: ( sourceId: string | number, targetId: string | number | null, @@ -104,6 +111,7 @@ export function ProTable< action, texts, empty, + error, onSort, initialFilters, }: ProTableProps) { @@ -121,6 +129,8 @@ export function ProTable< const [rowSelection, setRowSelection] = useState({}); const [data, setData] = useState([]); const [rowCount, setRowCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [requestError, setRequestError] = useState(false); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 200, @@ -193,6 +203,8 @@ export function ProTable< const fetchData = async () => { if (loading.current) return; loading.current = true; + setIsLoading(true); + setRequestError(false); try { const response = await request( { @@ -205,10 +217,13 @@ export function ProTable< ); setData(response.list); setRowCount(response.total); - } catch (error) { - console.log("Fetch data error:", error); + } catch (_error) { + setData([]); + setRowCount(0); + setRequestError(true); } finally { loading.current = false; + setIsLoading(false); } }; const reset = async () => { @@ -316,7 +331,16 @@ export function ProTable< ))} - {table.getRowModel()?.rows?.length ? ( + {requestError ? ( + + + {error || } + + + ) : table.getRowModel()?.rows?.length ? ( onSort ? ( table.getRowModel().rows.map((row) => ( - + {empty || } @@ -380,8 +407,12 @@ export function ProTable< - {loading.current && ( -
+ {isLoading && ( +
)} @@ -391,6 +422,16 @@ export function ProTable< ); } +function ProTableErrorState() { + return ( + + + ); +} + function createSelectColumn(): ColumnDef { return { id: "selected",