批量删除
This commit is contained in:
parent
f12b2767c0
commit
4874bba9ac
@ -15,7 +15,17 @@
|
|||||||
"operationId": "listAchievementDefinitions",
|
"operationId": "listAchievementDefinitions",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/components/responses/EmptyResponse"
|
"description": "批量创建的礼物",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-permission": "achievement:view",
|
"x-permission": "achievement:view",
|
||||||
@ -25,7 +35,17 @@
|
|||||||
"operationId": "createAchievementDefinition",
|
"operationId": "createAchievementDefinition",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/components/responses/EmptyResponse"
|
"description": "批量删除的资源",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-permission": "achievement:create",
|
"x-permission": "achievement:create",
|
||||||
@ -2353,6 +2373,21 @@
|
|||||||
"x-permissions": ["gift:create"]
|
"x-permissions": ["gift:create"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/gifts/batch": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "batchCreateGifts",
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/BatchCreateGiftsRequest"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "gift:create",
|
||||||
|
"x-permissions": ["gift:create"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/gifts/{gift_id}": {
|
"/admin/gifts/{gift_id}": {
|
||||||
"put": {
|
"put": {
|
||||||
"operationId": "updateGift",
|
"operationId": "updateGift",
|
||||||
@ -4205,6 +4240,21 @@
|
|||||||
"x-permissions": ["resource:create"]
|
"x-permissions": ["resource:create"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/resources/batch-delete": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "batchDeleteResources",
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/BatchDeleteResourcesRequest"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "resource:delete",
|
||||||
|
"x-permissions": ["resource:delete"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/resources/{resource_id}": {
|
"/admin/resources/{resource_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getResource",
|
"operationId": "getResource",
|
||||||
@ -5955,6 +6005,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestBodies": {
|
"requestBodies": {
|
||||||
|
"BatchCreateGiftsRequest": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["items"],
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BatchDeleteResourcesRequest": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["resourceIds"],
|
||||||
|
"properties": {
|
||||||
|
"resourceIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"BatchStatusRequest": {
|
"BatchStatusRequest": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { afterEach, expect, test, vi } from "vitest";
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
import { setAccessToken } from "@/shared/api/request";
|
import { setAccessToken } from "@/shared/api/request";
|
||||||
import {
|
import {
|
||||||
|
batchCreateGifts,
|
||||||
|
batchDeleteResources,
|
||||||
createEmojiPack,
|
createEmojiPack,
|
||||||
createGift,
|
createGift,
|
||||||
createResource,
|
createResource,
|
||||||
@ -85,6 +87,7 @@ test("resource APIs use generated admin paths", async () => {
|
|||||||
status: "active",
|
status: "active",
|
||||||
});
|
});
|
||||||
await deleteResource(11);
|
await deleteResource(11);
|
||||||
|
await batchDeleteResources([11, 12]);
|
||||||
await listGifts({ page: 1, page_size: 10, region_id: 1001 });
|
await listGifts({ page: 1, page_size: 10, region_id: 1001 });
|
||||||
await createGift({
|
await createGift({
|
||||||
chargeAssetType: "COIN",
|
chargeAssetType: "COIN",
|
||||||
@ -104,6 +107,26 @@ test("resource APIs use generated admin paths", async () => {
|
|||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
status: "active",
|
status: "active",
|
||||||
});
|
});
|
||||||
|
await batchCreateGifts([
|
||||||
|
{
|
||||||
|
chargeAssetType: "COIN",
|
||||||
|
coinPrice: 20,
|
||||||
|
cpRelationType: "",
|
||||||
|
effectiveAtMs: 1777766400000,
|
||||||
|
effectiveFromMs: 0,
|
||||||
|
effectiveToMs: 0,
|
||||||
|
effectTypes: ["animation"],
|
||||||
|
giftId: "ring",
|
||||||
|
giftTypeCode: "normal",
|
||||||
|
name: "Ring",
|
||||||
|
presentationJson: "{}",
|
||||||
|
priceVersion: "default",
|
||||||
|
regionIds: [0],
|
||||||
|
resourceId: 12,
|
||||||
|
sortOrder: 1,
|
||||||
|
status: "active",
|
||||||
|
},
|
||||||
|
]);
|
||||||
await enableResource(11);
|
await enableResource(11);
|
||||||
await disableResource(11);
|
await disableResource(11);
|
||||||
await createResourceGroup({
|
await createResourceGroup({
|
||||||
@ -180,23 +203,25 @@ test("resource APIs use generated admin paths", async () => {
|
|||||||
const [createUrl, createInit] = vi.mocked(fetch).mock.calls[1];
|
const [createUrl, createInit] = vi.mocked(fetch).mock.calls[1];
|
||||||
const [updateUrl, updateInit] = vi.mocked(fetch).mock.calls[2];
|
const [updateUrl, updateInit] = vi.mocked(fetch).mock.calls[2];
|
||||||
const [deleteResourceUrl, deleteResourceInit] = vi.mocked(fetch).mock.calls[3];
|
const [deleteResourceUrl, deleteResourceInit] = vi.mocked(fetch).mock.calls[3];
|
||||||
const [giftListUrl, giftListInit] = vi.mocked(fetch).mock.calls[4];
|
const [batchDeleteResourceUrl, batchDeleteResourceInit] = vi.mocked(fetch).mock.calls[4];
|
||||||
const [createGiftUrl, createGiftInit] = vi.mocked(fetch).mock.calls[5];
|
const [giftListUrl, giftListInit] = vi.mocked(fetch).mock.calls[5];
|
||||||
const [enableUrl, enableInit] = vi.mocked(fetch).mock.calls[6];
|
const [createGiftUrl, createGiftInit] = vi.mocked(fetch).mock.calls[6];
|
||||||
const [disableUrl, disableInit] = vi.mocked(fetch).mock.calls[7];
|
const [batchCreateGiftUrl, batchCreateGiftInit] = vi.mocked(fetch).mock.calls[7];
|
||||||
const [createGroupUrl, createGroupInit] = vi.mocked(fetch).mock.calls[8];
|
const [enableUrl, enableInit] = vi.mocked(fetch).mock.calls[8];
|
||||||
const [updateGroupUrl, updateGroupInit] = vi.mocked(fetch).mock.calls[9];
|
const [disableUrl, disableInit] = vi.mocked(fetch).mock.calls[9];
|
||||||
const [enableGroupUrl, enableGroupInit] = vi.mocked(fetch).mock.calls[10];
|
const [createGroupUrl, createGroupInit] = vi.mocked(fetch).mock.calls[10];
|
||||||
const [disableGroupUrl, disableGroupInit] = vi.mocked(fetch).mock.calls[11];
|
const [updateGroupUrl, updateGroupInit] = vi.mocked(fetch).mock.calls[11];
|
||||||
const [updateGiftUrl, updateGiftInit] = vi.mocked(fetch).mock.calls[12];
|
const [enableGroupUrl, enableGroupInit] = vi.mocked(fetch).mock.calls[12];
|
||||||
const [enableGiftUrl, enableGiftInit] = vi.mocked(fetch).mock.calls[13];
|
const [disableGroupUrl, disableGroupInit] = vi.mocked(fetch).mock.calls[13];
|
||||||
const [disableGiftUrl, disableGiftInit] = vi.mocked(fetch).mock.calls[14];
|
const [updateGiftUrl, updateGiftInit] = vi.mocked(fetch).mock.calls[14];
|
||||||
const [deleteGiftUrl, deleteGiftInit] = vi.mocked(fetch).mock.calls[15];
|
const [enableGiftUrl, enableGiftInit] = vi.mocked(fetch).mock.calls[15];
|
||||||
const [grantListUrl, grantListInit] = vi.mocked(fetch).mock.calls[16];
|
const [disableGiftUrl, disableGiftInit] = vi.mocked(fetch).mock.calls[16];
|
||||||
const [grantTargetUrl, grantTargetInit] = vi.mocked(fetch).mock.calls[17];
|
const [deleteGiftUrl, deleteGiftInit] = vi.mocked(fetch).mock.calls[17];
|
||||||
const [grantResourceUrl, grantResourceInit] = vi.mocked(fetch).mock.calls[18];
|
const [grantListUrl, grantListInit] = vi.mocked(fetch).mock.calls[18];
|
||||||
const [grantGroupUrl, grantGroupInit] = vi.mocked(fetch).mock.calls[19];
|
const [grantTargetUrl, grantTargetInit] = vi.mocked(fetch).mock.calls[19];
|
||||||
const [revokeGrantUrl, revokeGrantInit] = vi.mocked(fetch).mock.calls[20];
|
const [grantResourceUrl, grantResourceInit] = vi.mocked(fetch).mock.calls[20];
|
||||||
|
const [grantGroupUrl, grantGroupInit] = vi.mocked(fetch).mock.calls[21];
|
||||||
|
const [revokeGrantUrl, revokeGrantInit] = vi.mocked(fetch).mock.calls[22];
|
||||||
|
|
||||||
expect(String(listUrl)).toContain("/api/v1/admin/resources?");
|
expect(String(listUrl)).toContain("/api/v1/admin/resources?");
|
||||||
expect(String(listUrl)).toContain("resource_type=gift");
|
expect(String(listUrl)).toContain("resource_type=gift");
|
||||||
@ -222,12 +247,18 @@ test("resource APIs use generated admin paths", async () => {
|
|||||||
});
|
});
|
||||||
expect(String(deleteResourceUrl)).toContain("/api/v1/admin/resources/11");
|
expect(String(deleteResourceUrl)).toContain("/api/v1/admin/resources/11");
|
||||||
expect(deleteResourceInit?.method).toBe("DELETE");
|
expect(deleteResourceInit?.method).toBe("DELETE");
|
||||||
|
expect(String(batchDeleteResourceUrl)).toContain("/api/v1/admin/resources/batch-delete");
|
||||||
|
expect(batchDeleteResourceInit?.method).toBe("POST");
|
||||||
|
expect(JSON.parse(String(batchDeleteResourceInit?.body))).toEqual({ resourceIds: [11, 12] });
|
||||||
expect(String(giftListUrl)).toContain("/api/v1/admin/gifts?");
|
expect(String(giftListUrl)).toContain("/api/v1/admin/gifts?");
|
||||||
expect(String(giftListUrl)).toContain("region_id=1001");
|
expect(String(giftListUrl)).toContain("region_id=1001");
|
||||||
expect(giftListInit?.method).toBe("GET");
|
expect(giftListInit?.method).toBe("GET");
|
||||||
expect(String(createGiftUrl)).toContain("/api/v1/admin/gifts");
|
expect(String(createGiftUrl)).toContain("/api/v1/admin/gifts");
|
||||||
expect(createGiftInit?.method).toBe("POST");
|
expect(createGiftInit?.method).toBe("POST");
|
||||||
expect(JSON.parse(String(createGiftInit?.body))).toMatchObject({ giftId: "rose", resourceId: 11 });
|
expect(JSON.parse(String(createGiftInit?.body))).toMatchObject({ giftId: "rose", resourceId: 11 });
|
||||||
|
expect(String(batchCreateGiftUrl)).toContain("/api/v1/admin/gifts/batch");
|
||||||
|
expect(batchCreateGiftInit?.method).toBe("POST");
|
||||||
|
expect(JSON.parse(String(batchCreateGiftInit?.body))).toMatchObject({ items: [{ giftId: "ring", resourceId: 12 }] });
|
||||||
expect(String(enableUrl)).toContain("/api/v1/admin/resources/11/enable");
|
expect(String(enableUrl)).toContain("/api/v1/admin/resources/11/enable");
|
||||||
expect(enableInit?.method).toBe("POST");
|
expect(enableInit?.method).toBe("POST");
|
||||||
expect(String(disableUrl)).toContain("/api/v1/admin/resources/11/disable");
|
expect(String(disableUrl)).toContain("/api/v1/admin/resources/11/disable");
|
||||||
|
|||||||
@ -224,6 +224,10 @@ export interface GiftPayload {
|
|||||||
regionIds: number[];
|
regionIds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchCreateGiftsPayload {
|
||||||
|
items: GiftPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GiftTypePayload {
|
export interface GiftTypePayload {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
tabName: string;
|
tabName: string;
|
||||||
@ -337,6 +341,10 @@ export interface ResourceShopItemsPayload {
|
|||||||
items: ResourceShopItemPayload[];
|
items: ResourceShopItemPayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchDeleteResourcesPayload {
|
||||||
|
resourceIds: EntityId[];
|
||||||
|
}
|
||||||
|
|
||||||
export function listResources(query: PageQuery = {}): Promise<ApiPage<ResourceDto>> {
|
export function listResources(query: PageQuery = {}): Promise<ApiPage<ResourceDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listResources;
|
const endpoint = API_ENDPOINTS.listResources;
|
||||||
return apiRequest<ApiPage<ResourceDto>>(apiEndpointPath(API_OPERATIONS.listResources), {
|
return apiRequest<ApiPage<ResourceDto>>(apiEndpointPath(API_OPERATIONS.listResources), {
|
||||||
@ -371,6 +379,17 @@ export function deleteResource(resourceId: EntityId): Promise<ResourceDto> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function batchDeleteResources(resourceIds: EntityId[]): Promise<ResourceDto[]> {
|
||||||
|
const endpoint = API_ENDPOINTS.batchDeleteResources;
|
||||||
|
return apiRequest<ResourceDto[], BatchDeleteResourcesPayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.batchDeleteResources),
|
||||||
|
{
|
||||||
|
body: { resourceIds },
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateResourceMp4Layouts(items: ResourceMp4LayoutUpdatePayload[]): Promise<ResourceDto[]> {
|
export function updateResourceMp4Layouts(items: ResourceMp4LayoutUpdatePayload[]): Promise<ResourceDto[]> {
|
||||||
return apiRequest<ResourceDto[], ResourceMp4LayoutsPayload>("/v1/admin/resources/mp4-layouts/batch", {
|
return apiRequest<ResourceDto[], ResourceMp4LayoutsPayload>("/v1/admin/resources/mp4-layouts/batch", {
|
||||||
body: { items },
|
body: { items },
|
||||||
@ -548,6 +567,14 @@ export function createGift(payload: GiftPayload): Promise<GiftDto> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function batchCreateGifts(payloads: GiftPayload[]): Promise<GiftDto[]> {
|
||||||
|
const endpoint = API_ENDPOINTS.batchCreateGifts;
|
||||||
|
return apiRequest<GiftDto[], BatchCreateGiftsPayload>(apiEndpointPath(API_OPERATIONS.batchCreateGifts), {
|
||||||
|
body: { items: payloads },
|
||||||
|
method: endpoint.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateGift(giftId: string, payload: GiftPayload): Promise<GiftDto> {
|
export function updateGift(giftId: string, payload: GiftPayload): Promise<GiftDto> {
|
||||||
const endpoint = API_ENDPOINTS.updateGift;
|
const endpoint = API_ENDPOINTS.updateGift;
|
||||||
return apiRequest<GiftDto, GiftPayload>(apiEndpointPath(API_OPERATIONS.updateGift, { gift_id: giftId }), {
|
return apiRequest<GiftDto, GiftPayload>(apiEndpointPath(API_OPERATIONS.updateGift, { gift_id: giftId }), {
|
||||||
|
|||||||
117
src/features/resources/hooks/useGiftListPageBatchCreate.test.jsx
Normal file
117
src/features/resources/hooks/useGiftListPageBatchCreate.test.jsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||||
|
import { useGiftListPage } from "./useResourcePages.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
batchCreateGifts: vi.fn(),
|
||||||
|
can: vi.fn(),
|
||||||
|
createGift: vi.fn(),
|
||||||
|
listGifts: vi.fn(),
|
||||||
|
listGiftTypes: vi.fn(),
|
||||||
|
listResources: vi.fn(),
|
||||||
|
reload: vi.fn(),
|
||||||
|
showToast: vi.fn(),
|
||||||
|
usePaginatedQuery: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/app/auth/AuthProvider.jsx", () => ({
|
||||||
|
useAuth: () => ({ can: mocks.can }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/hooks/usePaginatedQuery.js", () => ({
|
||||||
|
usePaginatedQuery: mocks.usePaginatedQuery,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/hooks/useRegionOptions.js", () => ({
|
||||||
|
useRegionOptions: () => ({ loadingRegions: false, regionOptions: [{ label: "全球", value: "0" }] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/ui/ConfirmProvider.jsx", () => ({
|
||||||
|
useConfirm: () => vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
||||||
|
useToast: () => ({ showToast: mocks.showToast }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/features/resources/api", async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
batchCreateGifts: mocks.batchCreateGifts,
|
||||||
|
createGift: mocks.createGift,
|
||||||
|
listGifts: mocks.listGifts,
|
||||||
|
listGiftTypes: mocks.listGiftTypes,
|
||||||
|
listResources: mocks.listResources,
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.can.mockReturnValue(true);
|
||||||
|
mocks.batchCreateGifts.mockResolvedValue([]);
|
||||||
|
mocks.createGift.mockResolvedValue({});
|
||||||
|
mocks.listGifts.mockResolvedValue({ items: [], page: 1, pageSize: 100, total: 0 });
|
||||||
|
mocks.listGiftTypes.mockResolvedValue([]);
|
||||||
|
mocks.listResources.mockResolvedValue({ items: [], page: 1, pageSize: 100, total: 0 });
|
||||||
|
mocks.reload.mockResolvedValue(undefined);
|
||||||
|
mocks.usePaginatedQuery.mockReturnValue({
|
||||||
|
data: { items: [], page: 1, pageSize: 50, total: 0 },
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
reload: mocks.reload,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creating multiple gift drafts calls the batch create endpoint once", async () => {
|
||||||
|
const { result } = renderHook(() => useGiftListPage());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.openCreateGift();
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(result.current.activeAction).toBe("create"));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.setForm({
|
||||||
|
...giftDraft({ giftId: "rose", name: "Rose", resourceId: "11" }),
|
||||||
|
items: [
|
||||||
|
giftDraft({ giftId: "rose", name: "Rose", resourceId: "11" }),
|
||||||
|
giftDraft({ giftId: "ring", name: "Ring", resourceId: "12" }),
|
||||||
|
],
|
||||||
|
resourceId: "11",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.submitGift({ preventDefault: vi.fn() });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.batchCreateGifts).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.batchCreateGifts).toHaveBeenCalledWith([
|
||||||
|
expect.objectContaining({ giftId: "rose", name: "Rose", resourceId: 11 }),
|
||||||
|
expect.objectContaining({ giftId: "ring", name: "Ring", resourceId: 12 }),
|
||||||
|
]);
|
||||||
|
expect(mocks.createGift).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.reload).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function giftDraft(patch = {}) {
|
||||||
|
return {
|
||||||
|
chargeAssetType: "COIN",
|
||||||
|
coinPrice: "10",
|
||||||
|
cpRelationType: "",
|
||||||
|
effectTypes: ["animation"],
|
||||||
|
effectiveFrom: "",
|
||||||
|
effectiveTo: "",
|
||||||
|
enabled: true,
|
||||||
|
giftId: "gift",
|
||||||
|
giftTypeCode: "normal",
|
||||||
|
name: "Gift",
|
||||||
|
presentationJson: "{}",
|
||||||
|
priceVersion: "default",
|
||||||
|
regionIds: ["0"],
|
||||||
|
resourceId: "1",
|
||||||
|
sortOrder: "0",
|
||||||
|
...patch,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|||||||
import { useResourceListPage } from "./useResourcePages.js";
|
import { useResourceListPage } from "./useResourcePages.js";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
|
batchDeleteResources: vi.fn(),
|
||||||
can: vi.fn(),
|
can: vi.fn(),
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
deleteResource: vi.fn(),
|
deleteResource: vi.fn(),
|
||||||
@ -29,12 +30,14 @@ vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
|||||||
|
|
||||||
vi.mock("@/features/resources/api", async (importOriginal) => ({
|
vi.mock("@/features/resources/api", async (importOriginal) => ({
|
||||||
...(await importOriginal()),
|
...(await importOriginal()),
|
||||||
|
batchDeleteResources: mocks.batchDeleteResources,
|
||||||
deleteResource: mocks.deleteResource,
|
deleteResource: mocks.deleteResource,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.can.mockReturnValue(true);
|
mocks.can.mockReturnValue(true);
|
||||||
mocks.confirm.mockResolvedValue(true);
|
mocks.confirm.mockResolvedValue(true);
|
||||||
|
mocks.batchDeleteResources.mockResolvedValue([]);
|
||||||
mocks.deleteResource.mockResolvedValue({});
|
mocks.deleteResource.mockResolvedValue({});
|
||||||
mocks.reload.mockResolvedValue(undefined);
|
mocks.reload.mockResolvedValue(undefined);
|
||||||
mocks.usePaginatedQuery.mockReturnValue({
|
mocks.usePaginatedQuery.mockReturnValue({
|
||||||
@ -76,14 +79,14 @@ test("batch resource delete hides successful rows without reloading the list", a
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(deletedResourceIds).toEqual([11, 12]);
|
expect(deletedResourceIds).toEqual([11, 12]);
|
||||||
expect(mocks.deleteResource).toHaveBeenNthCalledWith(1, 11);
|
expect(mocks.batchDeleteResources).toHaveBeenCalledWith([11, 12]);
|
||||||
expect(mocks.deleteResource).toHaveBeenNthCalledWith(2, 12);
|
expect(mocks.deleteResource).not.toHaveBeenCalled();
|
||||||
expect(mocks.reload).not.toHaveBeenCalled();
|
expect(mocks.reload).not.toHaveBeenCalled();
|
||||||
expect(result.current.data.items).toEqual([]);
|
expect(result.current.data.items).toEqual([]);
|
||||||
expect(mocks.showToast).toHaveBeenCalledWith("已删除 2 个资源", "success");
|
expect(mocks.showToast).toHaveBeenCalledWith("已删除 2 个资源", "success");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("batch resource delete continues after an item fails and hides successful rows", async () => {
|
test("failed batch resource delete keeps all rows visible", async () => {
|
||||||
mocks.usePaginatedQuery.mockReturnValue({
|
mocks.usePaginatedQuery.mockReturnValue({
|
||||||
data: {
|
data: {
|
||||||
items: [
|
items: [
|
||||||
@ -99,9 +102,7 @@ test("batch resource delete continues after an item fails and hides successful r
|
|||||||
loading: false,
|
loading: false,
|
||||||
reload: mocks.reload,
|
reload: mocks.reload,
|
||||||
});
|
});
|
||||||
mocks.deleteResource.mockImplementation((resourceId) =>
|
mocks.batchDeleteResources.mockRejectedValueOnce(new Error("delete failed"));
|
||||||
resourceId === 12 ? Promise.reject(new Error("delete failed")) : Promise.resolve({}),
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() => useResourceListPage());
|
const { result } = renderHook(() => useResourceListPage());
|
||||||
|
|
||||||
let deletedResourceIds = [];
|
let deletedResourceIds = [];
|
||||||
@ -113,12 +114,11 @@ test("batch resource delete continues after an item fails and hides successful r
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(deletedResourceIds).toEqual([11, 13]);
|
expect(deletedResourceIds).toEqual([]);
|
||||||
expect(mocks.deleteResource).toHaveBeenNthCalledWith(1, 11);
|
expect(mocks.batchDeleteResources).toHaveBeenCalledWith([11, 12, 13]);
|
||||||
expect(mocks.deleteResource).toHaveBeenNthCalledWith(2, 12);
|
expect(mocks.deleteResource).not.toHaveBeenCalled();
|
||||||
expect(mocks.deleteResource).toHaveBeenNthCalledWith(3, 13);
|
expect(result.current.data.items.map((resource) => resource.resourceId)).toEqual([11, 12, 13]);
|
||||||
expect(result.current.data.items.map((resource) => resource.resourceId)).toEqual([12]);
|
expect(mocks.showToast).toHaveBeenCalledWith("delete failed", "error");
|
||||||
expect(mocks.showToast).toHaveBeenCalledWith("已删除 2 个资源,1 个删除失败:delete failed", "error");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("failed resource delete keeps the row visible", async () => {
|
test("failed resource delete keeps the row visible", async () => {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
|||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js";
|
||||||
import {
|
import {
|
||||||
|
batchCreateGifts,
|
||||||
|
batchDeleteResources as requestBatchDeleteResources,
|
||||||
createEmojiPack,
|
createEmojiPack,
|
||||||
createGift,
|
createGift,
|
||||||
createResource,
|
createResource,
|
||||||
@ -593,32 +595,23 @@ export function useResourceListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deletedResourceIds = [];
|
const deletedResourceIds = [];
|
||||||
const failedDeletes = [];
|
|
||||||
setLoadingAction(
|
setLoadingAction(
|
||||||
targetResources.length > 1 ? "resource-delete-batch" : `resource-delete-${targetResources[0].resourceId}`,
|
targetResources.length > 1 ? "resource-delete-batch" : `resource-delete-${targetResources[0].resourceId}`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
for (const resource of targetResources) {
|
if (targetResources.length > 1) {
|
||||||
try {
|
await requestBatchDeleteResources(targetResources.map((resource) => resource.resourceId));
|
||||||
await requestDeleteResource(resource.resourceId);
|
deletedResourceIds.push(...targetResources.map((resource) => resource.resourceId));
|
||||||
deletedResourceIds.push(resource.resourceId);
|
} else {
|
||||||
} catch (err) {
|
await requestDeleteResource(targetResources[0].resourceId);
|
||||||
failedDeletes.push({ err, resource });
|
deletedResourceIds.push(targetResources[0].resourceId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hideResourceRows(deletedResourceIds);
|
hideResourceRows(deletedResourceIds);
|
||||||
if (failedDeletes.length) {
|
|
||||||
const firstError = failedDeletes[0].err?.message || "删除资源失败";
|
|
||||||
showToast(
|
|
||||||
deletedResourceIds.length
|
|
||||||
? `已删除 ${deletedResourceIds.length} 个资源,${failedDeletes.length} 个删除失败:${firstError}`
|
|
||||||
: firstError,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return deletedResourceIds;
|
|
||||||
}
|
|
||||||
showToast(targetResources.length > 1 ? `已删除 ${deletedResourceIds.length} 个资源` : "资源已删除", "success");
|
showToast(targetResources.length > 1 ? `已删除 ${deletedResourceIds.length} 个资源` : "资源已删除", "success");
|
||||||
return deletedResourceIds;
|
return deletedResourceIds;
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "删除资源失败", "error");
|
||||||
|
return [];
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingAction("");
|
setLoadingAction("");
|
||||||
}
|
}
|
||||||
@ -1510,9 +1503,12 @@ export function useGiftListPage() {
|
|||||||
draft,
|
draft,
|
||||||
payload: buildGiftPayload(parseForm(giftFormSchema, draft)),
|
payload: buildGiftPayload(parseForm(giftFormSchema, draft)),
|
||||||
}));
|
}));
|
||||||
for (const { draft, payload } of payloads) {
|
if (payloads.length > 1) {
|
||||||
await createGift(payload);
|
await batchCreateGifts(payloads.map(({ payload }) => payload));
|
||||||
createdDrafts.push(draft);
|
createdDrafts.push(...payloads.map(({ draft }) => draft));
|
||||||
|
} else {
|
||||||
|
await createGift(payloads[0].payload);
|
||||||
|
createdDrafts.push(payloads[0].draft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast(editing ? "礼物已更新" : `已创建 ${createDrafts.length} 个礼物`, "success");
|
showToast(editing ? "礼物已更新" : `已创建 ${createDrafts.length} 个礼物`, "success");
|
||||||
|
|||||||
@ -26,6 +26,8 @@ export const API_OPERATIONS = {
|
|||||||
appSetPassword: "appSetPassword",
|
appSetPassword: "appSetPassword",
|
||||||
appUnbanUser: "appUnbanUser",
|
appUnbanUser: "appUnbanUser",
|
||||||
appUpdateUser: "appUpdateUser",
|
appUpdateUser: "appUpdateUser",
|
||||||
|
batchCreateGifts: "batchCreateGifts",
|
||||||
|
batchDeleteResources: "batchDeleteResources",
|
||||||
batchUpdateUserStatus: "batchUpdateUserStatus",
|
batchUpdateUserStatus: "batchUpdateUserStatus",
|
||||||
cancelRoomPin: "cancelRoomPin",
|
cancelRoomPin: "cancelRoomPin",
|
||||||
changePassword: "changePassword",
|
changePassword: "changePassword",
|
||||||
@ -436,6 +438,20 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "app-user:update",
|
permission: "app-user:update",
|
||||||
permissions: ["app-user:update"]
|
permissions: ["app-user:update"]
|
||||||
},
|
},
|
||||||
|
batchCreateGifts: {
|
||||||
|
method: "POST",
|
||||||
|
operationId: API_OPERATIONS.batchCreateGifts,
|
||||||
|
path: "/v1/admin/gifts/batch",
|
||||||
|
permission: "gift:create",
|
||||||
|
permissions: ["gift:create"]
|
||||||
|
},
|
||||||
|
batchDeleteResources: {
|
||||||
|
method: "POST",
|
||||||
|
operationId: API_OPERATIONS.batchDeleteResources,
|
||||||
|
path: "/v1/admin/resources/batch-delete",
|
||||||
|
permission: "resource:delete",
|
||||||
|
permissions: ["resource:delete"]
|
||||||
|
},
|
||||||
batchUpdateUserStatus: {
|
batchUpdateUserStatus: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.batchUpdateUserStatus,
|
operationId: API_OPERATIONS.batchUpdateUserStatus,
|
||||||
|
|||||||
90
src/shared/api/generated/schema.d.ts
vendored
90
src/shared/api/generated/schema.d.ts
vendored
@ -1620,6 +1620,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/gifts/batch": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["batchCreateGifts"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/gifts/{gift_id}": {
|
"/admin/gifts/{gift_id}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -2660,6 +2676,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/resources/batch-delete": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["batchDeleteResources"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/resources/{resource_id}": {
|
"/admin/resources/{resource_id}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -4601,6 +4633,20 @@ export interface components {
|
|||||||
Status: string;
|
Status: string;
|
||||||
};
|
};
|
||||||
requestBodies: {
|
requestBodies: {
|
||||||
|
BatchCreateGiftsRequest: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
items: Record<string, never>[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
BatchDeleteResourcesRequest: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
resourceIds: number[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
BatchStatusRequest: {
|
BatchStatusRequest: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
@ -4729,7 +4775,15 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
200: components["responses"]["EmptyResponse"];
|
/** @description 批量创建的礼物 */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": Record<string, never>[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
createAchievementDefinition: {
|
createAchievementDefinition: {
|
||||||
@ -4741,7 +4795,15 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
200: components["responses"]["EmptyResponse"];
|
/** @description 批量删除的资源 */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": Record<string, never>[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
updateAchievementDefinition: {
|
updateAchievementDefinition: {
|
||||||
@ -6631,6 +6693,18 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
batchCreateGifts: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: components["requestBodies"]["BatchCreateGiftsRequest"];
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
updateGift: {
|
updateGift: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -7873,6 +7947,18 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
batchDeleteResources: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: components["requestBodies"]["BatchDeleteResourcesRequest"];
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
getResource: {
|
getResource: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user