修复(#97): 区分邀请管理加载失败状态
PR Check / Lint, Check, Test and Build (push) Has been cancelled
Build and Release / Build (push) Has been cancelled

This commit is contained in:
2026-05-27 19:13:27 -07:00
parent aeca20ad1e
commit 100b641552
5 changed files with 92 additions and 9 deletions
@@ -12,6 +12,8 @@
"inviter": "Inviter", "inviter": "Inviter",
"inviterGiftDays": "Inviter Gift Days", "inviterGiftDays": "Inviter Gift Days",
"inviterId": "Inviter ID", "inviterId": "Inviter ID",
"loadErrorDescription": "Refresh the table or try again later.",
"loadErrorTitle": "Failed to load invites",
"no": "No", "no": "No",
"orderCount": "Orders", "orderCount": "Orders",
"searchPlaceholder": "Email or phone", "searchPlaceholder": "Email or phone",
@@ -12,6 +12,8 @@
"inviter": "邀请人", "inviter": "邀请人",
"inviterGiftDays": "邀请人赠送天数", "inviterGiftDays": "邀请人赠送天数",
"inviterId": "邀请人 ID", "inviterId": "邀请人 ID",
"loadErrorDescription": "刷新表格或稍后重试。",
"loadErrorTitle": "邀请记录加载失败",
"no": "否", "no": "否",
"orderCount": "订单数", "orderCount": "订单数",
"searchPlaceholder": "邮箱/手机号关键词", "searchPlaceholder": "邮箱/手机号关键词",
+23 -2
View File
@@ -127,12 +127,33 @@ describe("InviteManagement", () => {
expect(await screen.findByText("No invite records")).not.toBeNull(); 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(<InviteManagement />);
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")); mockedGetAdminInviteList.mockRejectedValue(new Error("network failed"));
render(<InviteManagement />); render(<InviteManagement />);
await waitFor(() => expect(mockedGetAdminInviteList).toHaveBeenCalled()); 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();
}); });
}); });
+17
View File
@@ -1,5 +1,10 @@
"use client"; "use client";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@workspace/ui/components/alert";
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
@@ -12,6 +17,7 @@ import {
type ProTableActions, type ProTableActions,
} from "@workspace/ui/composed/pro-table/pro-table"; } from "@workspace/ui/composed/pro-table/pro-table";
import { getAdminInviteList } from "@workspace/ui/services/admin/invite"; import { getAdminInviteList } from "@workspace/ui/services/admin/invite";
import { CircleAlert } from "lucide-react";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Display } from "@/components/display"; import { Display } from "@/components/display";
@@ -158,6 +164,17 @@ export default function InviteManagement() {
}, },
]} ]}
empty={<Empty description={t("empty", "No invite records")} />} empty={<Empty description={t("empty", "No invite records")} />}
error={
<Alert className="mx-auto max-w-md" variant="destructive">
<CircleAlert aria-hidden="true" />
<AlertTitle>
{t("loadErrorTitle", "Failed to load invites")}
</AlertTitle>
<AlertDescription>
{t("loadErrorDescription", "Refresh the table or try again later.")}
</AlertDescription>
</Alert>
}
header={{ title: t("title", "Invite Management") }} header={{ title: t("title", "Invite Management") }}
params={[ params={[
{ {
@@ -39,7 +39,13 @@ import { SortableRow } from "@workspace/ui/composed/pro-table/sortable-row";
import { ProTableWrapper } from "@workspace/ui/composed/pro-table/wrapper"; import { ProTableWrapper } from "@workspace/ui/composed/pro-table/wrapper";
import { cn } from "@workspace/ui/lib/utils"; import { cn } from "@workspace/ui/lib/utils";
import { useSize } from "ahooks"; 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 type React from "react";
import { import {
Fragment, Fragment,
@@ -79,6 +85,7 @@ export interface ProTableProps<TData, TValue> {
selectedRowsText: (total: number) => string; selectedRowsText: (total: number) => string;
}>; }>;
empty?: React.ReactNode; empty?: React.ReactNode;
error?: React.ReactNode;
onSort?: ( onSort?: (
sourceId: string | number, sourceId: string | number,
targetId: string | number | null, targetId: string | number | null,
@@ -104,6 +111,7 @@ export function ProTable<
action, action,
texts, texts,
empty, empty,
error,
onSort, onSort,
initialFilters, initialFilters,
}: ProTableProps<TData, TValue>) { }: ProTableProps<TData, TValue>) {
@@ -121,6 +129,8 @@ export function ProTable<
const [rowSelection, setRowSelection] = useState({}); const [rowSelection, setRowSelection] = useState({});
const [data, setData] = useState<TData[]>([]); const [data, setData] = useState<TData[]>([]);
const [rowCount, setRowCount] = useState<number>(0); const [rowCount, setRowCount] = useState<number>(0);
const [isLoading, setIsLoading] = useState(false);
const [requestError, setRequestError] = useState(false);
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: 200, pageSize: 200,
@@ -193,6 +203,8 @@ export function ProTable<
const fetchData = async () => { const fetchData = async () => {
if (loading.current) return; if (loading.current) return;
loading.current = true; loading.current = true;
setIsLoading(true);
setRequestError(false);
try { try {
const response = await request( const response = await request(
{ {
@@ -205,10 +217,13 @@ export function ProTable<
); );
setData(response.list); setData(response.list);
setRowCount(response.total); setRowCount(response.total);
} catch (error) { } catch (_error) {
console.log("Fetch data error:", error); setData([]);
setRowCount(0);
setRequestError(true);
} finally { } finally {
loading.current = false; loading.current = false;
setIsLoading(false);
} }
}; };
const reset = async () => { const reset = async () => {
@@ -316,7 +331,16 @@ export function ProTable<
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel()?.rows?.length ? ( {requestError ? (
<TableRow>
<TableCell
className="py-12"
colSpan={table.getAllLeafColumns().length}
>
{error || <ProTableErrorState />}
</TableCell>
</TableRow>
) : table.getRowModel()?.rows?.length ? (
onSort ? ( onSort ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<SortableRow <SortableRow
@@ -371,7 +395,10 @@ export function ProTable<
) )
) : ( ) : (
<TableRow> <TableRow>
<TableCell className="py-24" colSpan={columns.length + 2}> <TableCell
className="py-24"
colSpan={table.getAllLeafColumns().length}
>
{empty || <Empty />} {empty || <Empty />}
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -380,8 +407,12 @@ export function ProTable<
</Table> </Table>
</ProTableWrapper> </ProTableWrapper>
{loading.current && ( {isLoading && (
<div className="absolute top-0 z-20 flex h-full w-full items-center justify-center bg-muted/80"> <div
aria-label="Loading data"
className="absolute top-0 z-20 flex h-full w-full items-center justify-center bg-muted/80"
role="status"
>
<Loader className="h-4 w-4 animate-spin" /> <Loader className="h-4 w-4 animate-spin" />
</div> </div>
)} )}
@@ -391,6 +422,16 @@ export function ProTable<
); );
} }
function ProTableErrorState() {
return (
<Alert className="mx-auto max-w-md" variant="destructive">
<CircleAlert aria-hidden="true" />
<AlertTitle>Failed to load data</AlertTitle>
<AlertDescription>Refresh the table or try again later.</AlertDescription>
</Alert>
);
}
function createSelectColumn<TData, TValue>(): ColumnDef<TData, TValue> { function createSelectColumn<TData, TValue>(): ColumnDef<TData, TValue> {
return { return {
id: "selected", id: "selected",