diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 183999a..f4423aa 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -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": { diff --git a/src/app/permissions.ts b/src/app/permissions.ts index 2f176fd..bc86dce 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -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", diff --git a/src/features/resources/api.test.ts b/src/features/resources/api.test.ts index 0430b11..489d8c8 100644 --- a/src/features/resources/api.test.ts +++ b/src/features/resources/api.test.ts @@ -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"); diff --git a/src/features/resources/api.ts b/src/features/resources/api.ts index ea745f7..28bd42c 100644 --- a/src/features/resources/api.ts +++ b/src/features/resources/api.ts @@ -364,6 +364,13 @@ export function updateResource(resourceId: EntityId, payload: ResourcePayload): ); } +export function deleteResource(resourceId: EntityId): Promise { + const endpoint = API_ENDPOINTS.deleteResource; + return apiRequest(apiEndpointPath(API_OPERATIONS.deleteResource, { resource_id: resourceId }), { + method: endpoint.method, + }); +} + export function updateResourceMp4Layouts(items: ResourceMp4LayoutUpdatePayload[]): Promise { return apiRequest("/v1/admin/resources/mp4-layouts/batch", { body: { items }, diff --git a/src/features/resources/hooks/useResourceListPageDelete.test.jsx b/src/features/resources/hooks/useResourceListPageDelete.test.jsx new file mode 100644 index 0000000..418777a --- /dev/null +++ b/src/features/resources/hooks/useResourceListPageDelete.test.jsx @@ -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, + }; +} diff --git a/src/features/resources/hooks/useResourcePages.js b/src/features/resources/hooks/useResourcePages.js index 7deff02..9b49ac6 100644 --- a/src/features/resources/hooks/useResourcePages.js +++ b/src/features/resources/hooks/useResourcePages.js @@ -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, diff --git a/src/features/resources/pages/ResourceListPage.jsx b/src/features/resources/pages/ResourceListPage.jsx index 2956552..f535bfe 100644 --- a/src/features/resources/pages/ResourceListPage.jsx +++ b/src/features/resources/pages/ResourceListPage.jsx @@ -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: ( toggleAllResources(event.target.checked)} /> ), @@ -208,8 +213,10 @@ export function ResourceListPage() { return ( toggleResourceSelection(resource, event.target.checked)} /> @@ -257,7 +264,13 @@ export function ResourceListPage() { : column.key === "actions" ? { ...column, - render: (resource) => , + render: (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() { + {page.abilities.canDelete ? ( + + {deletingBatch ? ( + + ) : ( + + )} + + ) : null} {page.abilities.canUpdate ? ( { if (!mp4AnimationSource || !page.abilities.canUpdate || loading) { @@ -927,6 +968,15 @@ function ResourceRowActions({ page, resource }) { > + {page.abilities.canDelete ? ( + page.deleteResources([resource])} + > + {deleting ? : } + + ) : null} ); } diff --git a/src/features/resources/pages/ResourceListPage.test.jsx b/src/features/resources/pages/ResourceListPage.test.jsx new file mode 100644 index 0000000..0e8453d --- /dev/null +++ b/src/features/resources/pages/ResourceListPage.test.jsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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, + }; +} diff --git a/src/features/resources/permissions.js b/src/features/resources/permissions.js index 53a5e20..12bc640 100644 --- a/src/features/resources/permissions.js +++ b/src/features/resources/permissions.js @@ -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), diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 260ef5d..014f5f6 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -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 = { 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, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 518790f..a7005f7 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -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;