资源删除

This commit is contained in:
zhx 2026-07-05 14:08:06 +08:00
parent 53a178f11c
commit f12b2767c0
11 changed files with 502 additions and 25 deletions

View File

@ -4245,6 +4245,26 @@
],
"x-permission": "resource:update",
"x-permissions": ["resource:update"]
},
"delete": {
"operationId": "deleteResource",
"responses": {
"200": {
"$ref": "#/components/responses/EmptyResponse"
}
},
"parameters": [
{
"in": "path",
"name": "resource_id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"x-permission": "resource:delete",
"x-permissions": ["resource:delete"]
}
},
"/admin/resources/{resource_id}/disable": {

View File

@ -134,6 +134,7 @@ export const PERMISSIONS = {
resourceView: "resource:view",
resourceCreate: "resource:create",
resourceUpdate: "resource:update",
resourceDelete: "resource:delete",
resourceShopView: "resource-shop:view",
resourceShopUpdate: "resource-shop:update",
resourceGroupView: "resource-group:view",

View File

@ -10,6 +10,7 @@ import {
disableResource,
disableResourceGroup,
deleteGift,
deleteResource,
enableResourceShopItem,
enableGift,
enableResource,
@ -83,6 +84,7 @@ test("resource APIs use generated admin paths", async () => {
sortOrder: 0,
status: "active",
});
await deleteResource(11);
await listGifts({ page: 1, page_size: 10, region_id: 1001 });
await createGift({
chargeAssetType: "COIN",
@ -177,23 +179,24 @@ test("resource APIs use generated admin paths", async () => {
const [listUrl, listInit] = vi.mocked(fetch).mock.calls[0];
const [createUrl, createInit] = vi.mocked(fetch).mock.calls[1];
const [updateUrl, updateInit] = vi.mocked(fetch).mock.calls[2];
const [giftListUrl, giftListInit] = vi.mocked(fetch).mock.calls[3];
const [createGiftUrl, createGiftInit] = vi.mocked(fetch).mock.calls[4];
const [enableUrl, enableInit] = vi.mocked(fetch).mock.calls[5];
const [disableUrl, disableInit] = vi.mocked(fetch).mock.calls[6];
const [createGroupUrl, createGroupInit] = vi.mocked(fetch).mock.calls[7];
const [updateGroupUrl, updateGroupInit] = vi.mocked(fetch).mock.calls[8];
const [enableGroupUrl, enableGroupInit] = vi.mocked(fetch).mock.calls[9];
const [disableGroupUrl, disableGroupInit] = vi.mocked(fetch).mock.calls[10];
const [updateGiftUrl, updateGiftInit] = vi.mocked(fetch).mock.calls[11];
const [enableGiftUrl, enableGiftInit] = vi.mocked(fetch).mock.calls[12];
const [disableGiftUrl, disableGiftInit] = vi.mocked(fetch).mock.calls[13];
const [deleteGiftUrl, deleteGiftInit] = vi.mocked(fetch).mock.calls[14];
const [grantListUrl, grantListInit] = vi.mocked(fetch).mock.calls[15];
const [grantTargetUrl, grantTargetInit] = vi.mocked(fetch).mock.calls[16];
const [grantResourceUrl, grantResourceInit] = vi.mocked(fetch).mock.calls[17];
const [grantGroupUrl, grantGroupInit] = vi.mocked(fetch).mock.calls[18];
const [revokeGrantUrl, revokeGrantInit] = vi.mocked(fetch).mock.calls[19];
const [deleteResourceUrl, deleteResourceInit] = vi.mocked(fetch).mock.calls[3];
const [giftListUrl, giftListInit] = vi.mocked(fetch).mock.calls[4];
const [createGiftUrl, createGiftInit] = vi.mocked(fetch).mock.calls[5];
const [enableUrl, enableInit] = vi.mocked(fetch).mock.calls[6];
const [disableUrl, disableInit] = vi.mocked(fetch).mock.calls[7];
const [createGroupUrl, createGroupInit] = vi.mocked(fetch).mock.calls[8];
const [updateGroupUrl, updateGroupInit] = vi.mocked(fetch).mock.calls[9];
const [enableGroupUrl, enableGroupInit] = vi.mocked(fetch).mock.calls[10];
const [disableGroupUrl, disableGroupInit] = vi.mocked(fetch).mock.calls[11];
const [updateGiftUrl, updateGiftInit] = vi.mocked(fetch).mock.calls[12];
const [enableGiftUrl, enableGiftInit] = vi.mocked(fetch).mock.calls[13];
const [disableGiftUrl, disableGiftInit] = vi.mocked(fetch).mock.calls[14];
const [deleteGiftUrl, deleteGiftInit] = vi.mocked(fetch).mock.calls[15];
const [grantListUrl, grantListInit] = vi.mocked(fetch).mock.calls[16];
const [grantTargetUrl, grantTargetInit] = vi.mocked(fetch).mock.calls[17];
const [grantResourceUrl, grantResourceInit] = vi.mocked(fetch).mock.calls[18];
const [grantGroupUrl, grantGroupInit] = vi.mocked(fetch).mock.calls[19];
const [revokeGrantUrl, revokeGrantInit] = vi.mocked(fetch).mock.calls[20];
expect(String(listUrl)).toContain("/api/v1/admin/resources?");
expect(String(listUrl)).toContain("resource_type=gift");
@ -217,6 +220,8 @@ test("resource APIs use generated admin paths", async () => {
badgeKind: "normal",
metadataJson: '{"profile_card_layout":{"content_top":120,"content_height":1500}}',
});
expect(String(deleteResourceUrl)).toContain("/api/v1/admin/resources/11");
expect(deleteResourceInit?.method).toBe("DELETE");
expect(String(giftListUrl)).toContain("/api/v1/admin/gifts?");
expect(String(giftListUrl)).toContain("region_id=1001");
expect(giftListInit?.method).toBe("GET");

View File

@ -364,6 +364,13 @@ export function updateResource(resourceId: EntityId, payload: ResourcePayload):
);
}
export function deleteResource(resourceId: EntityId): Promise<ResourceDto> {
const endpoint = API_ENDPOINTS.deleteResource;
return apiRequest<ResourceDto>(apiEndpointPath(API_OPERATIONS.deleteResource, { resource_id: resourceId }), {
method: endpoint.method,
});
}
export function updateResourceMp4Layouts(items: ResourceMp4LayoutUpdatePayload[]): Promise<ResourceDto[]> {
return apiRequest<ResourceDto[], ResourceMp4LayoutsPayload>("/v1/admin/resources/mp4-layouts/batch", {
body: { items },

View File

@ -0,0 +1,158 @@
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { useResourceListPage } from "./useResourcePages.js";
const mocks = vi.hoisted(() => ({
can: vi.fn(),
confirm: vi.fn(),
deleteResource: 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/ui/ConfirmProvider.jsx", () => ({
useConfirm: () => mocks.confirm,
}));
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
useToast: () => ({ showToast: mocks.showToast }),
}));
vi.mock("@/features/resources/api", async (importOriginal) => ({
...(await importOriginal()),
deleteResource: mocks.deleteResource,
}));
beforeEach(() => {
mocks.can.mockReturnValue(true);
mocks.confirm.mockResolvedValue(true);
mocks.deleteResource.mockResolvedValue({});
mocks.reload.mockResolvedValue(undefined);
mocks.usePaginatedQuery.mockReturnValue({
data: resourcePage(),
error: null,
loading: false,
reload: mocks.reload,
});
});
afterEach(() => {
vi.clearAllMocks();
});
test("deleting a resource hides only that row without reloading the list", async () => {
const { result } = renderHook(() => useResourceListPage());
let deletedResourceIds = [];
await act(async () => {
deletedResourceIds = await result.current.deleteResources([resourceFixture({ resourceId: 11 })]);
});
expect(deletedResourceIds).toEqual([11]);
expect(mocks.deleteResource).toHaveBeenCalledWith(11);
expect(mocks.reload).not.toHaveBeenCalled();
expect(result.current.data.items.map((resource) => resource.resourceId)).toEqual([12]);
expect(mocks.showToast).toHaveBeenCalledWith("资源已删除", "success");
});
test("batch resource delete hides successful rows without reloading the list", async () => {
const { result } = renderHook(() => useResourceListPage());
let deletedResourceIds = [];
await act(async () => {
deletedResourceIds = await result.current.deleteResources([
resourceFixture({ resourceId: 11 }),
resourceFixture({ resourceId: 12 }),
]);
});
expect(deletedResourceIds).toEqual([11, 12]);
expect(mocks.deleteResource).toHaveBeenNthCalledWith(1, 11);
expect(mocks.deleteResource).toHaveBeenNthCalledWith(2, 12);
expect(mocks.reload).not.toHaveBeenCalled();
expect(result.current.data.items).toEqual([]);
expect(mocks.showToast).toHaveBeenCalledWith("已删除 2 个资源", "success");
});
test("batch resource delete continues after an item fails and hides successful rows", async () => {
mocks.usePaginatedQuery.mockReturnValue({
data: {
items: [
resourceFixture({ resourceId: 11 }),
resourceFixture({ resourceId: 12 }),
resourceFixture({ resourceId: 13 }),
],
page: 1,
pageSize: 50,
total: 3,
},
error: null,
loading: false,
reload: mocks.reload,
});
mocks.deleteResource.mockImplementation((resourceId) =>
resourceId === 12 ? Promise.reject(new Error("delete failed")) : Promise.resolve({}),
);
const { result } = renderHook(() => useResourceListPage());
let deletedResourceIds = [];
await act(async () => {
deletedResourceIds = await result.current.deleteResources([
resourceFixture({ resourceId: 11 }),
resourceFixture({ resourceId: 12 }),
resourceFixture({ resourceId: 13 }),
]);
});
expect(deletedResourceIds).toEqual([11, 13]);
expect(mocks.deleteResource).toHaveBeenNthCalledWith(1, 11);
expect(mocks.deleteResource).toHaveBeenNthCalledWith(2, 12);
expect(mocks.deleteResource).toHaveBeenNthCalledWith(3, 13);
expect(result.current.data.items.map((resource) => resource.resourceId)).toEqual([12]);
expect(mocks.showToast).toHaveBeenCalledWith("已删除 2 个资源1 个删除失败delete failed", "error");
});
test("failed resource delete keeps the row visible", async () => {
mocks.deleteResource.mockRejectedValueOnce(new Error("delete failed"));
const { result } = renderHook(() => useResourceListPage());
let deletedResourceIds = [];
await act(async () => {
deletedResourceIds = await result.current.deleteResources([resourceFixture({ resourceId: 11 })]);
});
expect(deletedResourceIds).toEqual([]);
expect(mocks.reload).not.toHaveBeenCalled();
expect(result.current.data.items.map((resource) => resource.resourceId)).toEqual([11, 12]);
expect(mocks.showToast).toHaveBeenCalledWith("delete failed", "error");
});
function resourcePage() {
return {
items: [resourceFixture({ resourceId: 11, name: "Rose" }), resourceFixture({ resourceId: 12, name: "Ring" })],
page: 1,
pageSize: 50,
total: 2,
};
}
function resourceFixture(patch = {}) {
return {
coinPrice: 10,
name: "Rose",
resourceCode: "gift_rose",
resourceId: 11,
resourceType: "gift",
status: "active",
...patch,
};
}

View File

@ -11,6 +11,7 @@ import {
createResource,
createResourceGroup,
deleteGift as requestDeleteGift,
deleteResource as requestDeleteResource,
disableResourceShopItem,
disableGift,
disableResource,
@ -391,6 +392,7 @@ function groupItemToForm(item) {
export function useResourceListPage() {
const abilities = useResourceAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
const [query, setQuery] = useState("");
const [status, setStatus] = useState("");
@ -460,6 +462,14 @@ export function useResourceListPage() {
setHiddenResourceIds((current) => (current.includes(resourceId) ? current : [...current, resourceId]));
};
const hideResourceRows = (resourceIds) => {
const nextIds = resourceIds.filter(Boolean);
if (!nextIds.length) {
return;
}
setHiddenResourceIds((current) => Array.from(new Set([...current, ...nextIds])));
};
const reloadResources = async () => {
setResourceRowPatches({});
setHiddenResourceIds([]);
@ -562,6 +572,58 @@ export function useResourceListPage() {
);
};
const deleteResources = async (resources = []) => {
const targetResources = resources.filter((resource) => resource?.resourceId);
if (!abilities.canDelete || !targetResources.length) {
return [];
}
const title = targetResources.length > 1 ? "批量删除资源" : "删除资源";
const message =
targetResources.length > 1
? `确认删除选中的 ${targetResources.length} 个资源?删除成功后资源会从资源列表移除。`
: `确认删除资源 ${targetResources[0].name || targetResources[0].resourceCode || targetResources[0].resourceId}?删除成功后资源会从资源列表移除。`;
const ok = await confirm({
confirmText: "删除",
message,
title,
tone: "danger",
});
if (!ok) {
return [];
}
const deletedResourceIds = [];
const failedDeletes = [];
setLoadingAction(
targetResources.length > 1 ? "resource-delete-batch" : `resource-delete-${targetResources[0].resourceId}`,
);
try {
for (const resource of targetResources) {
try {
await requestDeleteResource(resource.resourceId);
deletedResourceIds.push(resource.resourceId);
} catch (err) {
failedDeletes.push({ err, resource });
}
}
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");
return deletedResourceIds;
} finally {
setLoadingAction("");
}
};
const runAction = async (action, successMessage, submitter) => {
setLoadingAction(action);
try {
@ -588,6 +650,7 @@ export function useResourceListPage() {
changeResourceType: resetSetter(setResourceType, setPage),
changeStatus: resetSetter(setStatus, setPage),
closeAction,
deleteResources,
form,
loadingAction,
openCreateResource,

View File

@ -1,4 +1,5 @@
import Add from "@mui/icons-material/Add";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import EditOutlined from "@mui/icons-material/EditOutlined";
import FileUploadOutlined from "@mui/icons-material/FileUploadOutlined";
import HelpOutlineOutlined from "@mui/icons-material/HelpOutlineOutlined";
@ -163,6 +164,9 @@ export function ResourceListPage() {
const selectedOnPageCount = items.filter((resource) => selectedIdSet.has(resource.resourceId)).length;
const someChecked = selectedOnPageCount > 0 && !allChecked;
const batchMp4Running = batchMp4Progress.running;
const deletingResources = page.loadingAction.startsWith("resource-delete-");
const deletingBatch = page.loadingAction === "resource-delete-batch";
const canSelectResources = page.abilities.canUpdate || page.abilities.canDelete;
useEffect(() => {
const itemIds = new Set(items.map((resource) => resource.resourceId));
@ -196,9 +200,10 @@ export function ResourceListPage() {
header: (
<Checkbox
checked={allChecked}
disabled={!page.abilities.canUpdate || items.length === 0 || batchMp4Running}
disabled={!canSelectResources || items.length === 0 || batchMp4Running || deletingResources}
indeterminate={someChecked}
size="small"
slotProps={{ input: { "aria-label": "选择当前页资源" } }}
onChange={(event) => toggleAllResources(event.target.checked)}
/>
),
@ -208,8 +213,10 @@ export function ResourceListPage() {
return (
<Checkbox
checked={selectedIdSet.has(resource.resourceId)}
disabled={!page.abilities.canUpdate || batchMp4Running}
inputProps={{ "aria-label": `选择 ${resource.name || resource.resourceCode || resource.resourceId}` }}
disabled={!canSelectResources || batchMp4Running || deletingResources}
slotProps={{
input: { "aria-label": `选择 ${resource.name || resource.resourceCode || resource.resourceId}` },
}}
size="small"
onChange={(event) => toggleResourceSelection(resource, event.target.checked)}
/>
@ -257,7 +264,13 @@ export function ResourceListPage() {
: column.key === "actions"
? {
...column,
render: (resource) => <ResourceRowActions page={page} resource={resource} />,
render: (resource) => (
<ResourceRowActions
deleteDisabled={batchMp4Running || Boolean(mp4BatchResult)}
page={page}
resource={resource}
/>
),
}
: column,
),
@ -394,6 +407,18 @@ export function ResourceListPage() {
setMp4BatchResult({ ignoredCount, layoutCounts, rows: updates });
};
const deleteSelectedResources = async () => {
if (!page.abilities.canDelete || !selectedResources.length || deletingResources) {
return;
}
const deletedResourceIds = await page.deleteResources(selectedResources);
if (!deletedResourceIds.length) {
return;
}
const deletedIdSet = new Set(deletedResourceIds);
setSelectedResourceIds((current) => current.filter((resourceId) => !deletedIdSet.has(resourceId)));
};
const writeMp4BatchResult = async () => {
const rows = mp4BatchResult?.rows || [];
if (!rows.length || batchMp4Running) {
@ -504,11 +529,24 @@ export function ResourceListPage() {
<AdminListPage>
<AdminListToolbar
actions={
page.abilities.canCreate || page.abilities.canUpdate ? (
page.abilities.canCreate || page.abilities.canUpdate || page.abilities.canDelete ? (
<>
{page.abilities.canDelete ? (
<AdminActionIconButton
disabled={!selectedResources.length || deletingResources || batchMp4Running}
label={deletingBatch ? "批量删除中" : `批量删除资源(${selectedResources.length}`}
onClick={deleteSelectedResources}
>
{deletingBatch ? (
<CircularProgress size={16} />
) : (
<DeleteOutlineOutlined fontSize="small" />
)}
</AdminActionIconButton>
) : null}
{page.abilities.canUpdate ? (
<AdminActionIconButton
disabled={!selectedResources.length || batchMp4Running}
disabled={!selectedResources.length || batchMp4Running || deletingResources}
label={
batchMp4Running
? batchMp4Progress.label || "批量重算 MP4"
@ -885,11 +923,14 @@ function Mp4BatchResultDialog({ loading, onClose, onSubmit, open, result }) {
);
}
function ResourceRowActions({ page, resource }) {
function ResourceRowActions({ deleteDisabled = false, page, resource }) {
const { showToast } = useToast();
const [recalculating, setRecalculating] = useState(false);
const mp4AnimationSource = mp4ResourceAnimationSource(resource);
const loading = recalculating || page.loadingAction === `resource-mp4-layout-${resource.resourceId}`;
const deleting = page.loadingAction === `resource-delete-${resource.resourceId}`;
const deletingBatch = page.loadingAction === "resource-delete-batch";
const deletingResources = page.loadingAction.startsWith("resource-delete-");
const recalculateMp4Layout = async () => {
if (!mp4AnimationSource || !page.abilities.canUpdate || loading) {
@ -927,6 +968,15 @@ function ResourceRowActions({ page, resource }) {
>
<EditOutlined fontSize="small" />
</AdminActionIconButton>
{page.abilities.canDelete ? (
<AdminActionIconButton
disabled={deleteDisabled || deletingResources || deletingBatch}
label={deleting ? "删除中" : "删除资源"}
onClick={() => page.deleteResources([resource])}
>
{deleting ? <CircularProgress size={16} /> : <DeleteOutlineOutlined fontSize="small" />}
</AdminActionIconButton>
) : null}
</AdminRowActions>
);
}

View File

@ -0,0 +1,150 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { ResourceListPage } from "./ResourceListPage.jsx";
import { useResourceListPage } from "@/features/resources/hooks/useResourcePages.js";
const mocks = vi.hoisted(() => ({
deleteResources: vi.fn(),
showToast: vi.fn(),
}));
vi.mock("@/features/resources/hooks/useResourcePages.js", () => ({
useResourceListPage: vi.fn(),
}));
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
useToast: () => ({ showToast: mocks.showToast }),
}));
beforeEach(() => {
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
disconnect() {}
};
mocks.deleteResources.mockResolvedValue([]);
});
afterEach(() => {
vi.clearAllMocks();
});
test("resource list calls row delete action with the target resource", async () => {
const page = pageFixture();
vi.mocked(useResourceListPage).mockReturnValue(page);
render(<ResourceListPage />);
fireEvent.click(screen.getAllByRole("button", { name: "删除资源" })[0]);
await waitFor(() => {
expect(mocks.deleteResources).toHaveBeenCalledWith([page.data.items[0]]);
});
});
test("resource list batch delete uses selected resources and clears selected rows after success", async () => {
const page = pageFixture();
mocks.deleteResources.mockResolvedValue([11, 12]);
vi.mocked(useResourceListPage).mockReturnValue(page);
render(<ResourceListPage />);
fireEvent.click(screen.getByLabelText("选择 Rose"));
fireEvent.click(screen.getByLabelText("选择 Ring"));
fireEvent.click(screen.getByRole("button", { name: "批量删除资源2" }));
await waitFor(() => {
expect(mocks.deleteResources).toHaveBeenCalledWith(page.data.items);
});
await waitFor(() => {
expect(screen.getByLabelText("选择 Rose")).not.toBeChecked();
expect(screen.getByLabelText("选择 Ring")).not.toBeChecked();
});
});
test("resource list hides delete actions without delete permission", () => {
vi.mocked(useResourceListPage).mockReturnValue(
pageFixture({
abilities: {
canCreate: false,
canDelete: false,
canUpdate: true,
canUpload: true,
},
}),
);
render(<ResourceListPage />);
expect(screen.queryByLabelText(/批量删除资源/)).not.toBeInTheDocument();
expect(screen.queryByLabelText("删除资源")).not.toBeInTheDocument();
});
test("resource list disables other delete actions while a resource delete is running", () => {
vi.mocked(useResourceListPage).mockReturnValue(pageFixture({ loadingAction: "resource-delete-11" }));
render(<ResourceListPage />);
expect(screen.getByRole("button", { name: "删除中" })).toBeDisabled();
expect(screen.getByRole("button", { name: "删除资源" })).toBeDisabled();
expect(screen.getByRole("button", { name: "批量删除资源0" })).toBeDisabled();
expect(screen.getByLabelText("选择 Rose")).toBeDisabled();
});
function pageFixture(patch = {}) {
return {
abilities: {
canCreate: true,
canDelete: true,
canUpdate: true,
canUpload: true,
},
activeAction: "",
changeQuery: vi.fn(),
changeResourceType: vi.fn(),
changeStatus: vi.fn(),
closeAction: vi.fn(),
data: {
items: [resourceFixture({ resourceId: 11, name: "Rose" }), resourceFixture({ resourceId: 12, name: "Ring" })],
page: 1,
pageSize: 50,
total: 2,
},
deleteResources: mocks.deleteResources,
error: null,
form: {},
loading: false,
loadingAction: "",
openCreateResource: vi.fn(),
openEditResource: vi.fn(),
page: 1,
query: "",
reload: vi.fn(),
resetFilters: vi.fn(),
resourceType: "",
selectedResource: null,
setForm: vi.fn(),
setPage: vi.fn(),
status: "",
submitResource: vi.fn(),
toggleManagerGrant: vi.fn(),
toggleResource: vi.fn(),
...patch,
};
}
function resourceFixture(patch = {}) {
return {
assetUrl: "https://media.example.com/resource.png",
coinPrice: 10,
managerGrantEnabled: false,
name: "Rose",
previewUrl: "https://media.example.com/cover.png",
priceType: "coin",
resourceCode: "gift_rose",
resourceId: 11,
resourceType: "gift",
status: "active",
updatedAtMs: 1782287714973,
...patch,
};
}

View File

@ -11,6 +11,7 @@ export function useResourceAbilities() {
canCreateGroup: can(PERMISSIONS.resourceGroupCreate),
canRevokeGrant: can(PERMISSIONS.resourceGrantRevoke),
canCreateEmojiPack: can(PERMISSIONS.emojiPackCreate),
canDelete: can(PERMISSIONS.resourceDelete),
canDeleteGift: can(PERMISSIONS.giftDelete),
canStatusGift: can(PERMISSIONS.giftStatus),
canUpdateShop: can(PERMISSIONS.resourceShopUpdate),

View File

@ -87,6 +87,7 @@ export const API_OPERATIONS = {
deletePermission: "deletePermission",
deletePopup: "deletePopup",
deleteRechargeProduct: "deleteRechargeProduct",
deleteResource: "deleteResource",
deleteRole: "deleteRole",
deleteRoom: "deleteRoom",
deleteSplashScreen: "deleteSplashScreen",
@ -860,6 +861,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "payment-product:delete",
permissions: ["payment-product:delete"]
},
deleteResource: {
method: "DELETE",
operationId: API_OPERATIONS.deleteResource,
path: "/v1/admin/resources/{resource_id}",
permission: "resource:delete",
permissions: ["resource:delete"]
},
deleteRole: {
method: "DELETE",
operationId: API_OPERATIONS.deleteRole,

View File

@ -2670,7 +2670,7 @@ export interface paths {
get: operations["getResource"];
put: operations["updateResource"];
post?: never;
delete?: never;
delete: operations["deleteResource"];
options?: never;
head?: never;
patch?: never;
@ -7901,6 +7901,20 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
deleteResource: {
parameters: {
query?: never;
header?: never;
path: {
resource_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
disableResource: {
parameters: {
query?: never;