fix: allow deleting equipment

- Add `DELETE /api/equipments/:id` endpoint.
- Add `deleteEquipment` method to `EquipmentService` which also cleans up associated assets and generation references.
- Implement delete action in `Equipments` page UI.
- Pass `actions` prop to `VibeCard` to enable context menu.
This commit is contained in:
google-labs-jules[bot] committed 2026-02-16 02:55:09 +00:00
1 parent b2f6869563
commit 45f7d0b2ef
5 files changed
+120 -1

No files matched your search

+14
View File
@@ -4,6 +4,7 @@ import equipment from './equipment'
const mockEquipmentService = {
listEquipments: mock(),
updateEquipment: mock(),
deleteEquipment: mock(),
}
mock.module('../service/equipment', () => ({
@@ -14,6 +15,7 @@ describe('Equipment API', () => {
afterEach(() => {
mockEquipmentService.listEquipments.mockClear()
mockEquipmentService.updateEquipment.mockClear()
mockEquipmentService.deleteEquipment.mockClear()
})
it('GET / should list equipments with filters', async () => {
@@ -46,4 +48,16 @@ describe('Equipment API', () => {
name: 'Updated',
})
})
it('DELETE /:id should delete equipment', async () => {
mockEquipmentService.deleteEquipment.mockResolvedValue({ id: '1' })
const req = new Request('http://localhost/1', {
method: 'DELETE',
})
const res = await equipment.fetch(req)
expect(res.status).toBe(200)
expect(mockEquipmentService.deleteEquipment).toHaveBeenCalledWith('1')
})
})
+12 -1
View File
@@ -42,4 +42,15 @@ const routeWithPatch = route.patch('/:id', zValidator('json', patchSchema), asyn
}
})
export default routeWithPatch
const routeWithDelete = routeWithPatch.delete('/:id', async (c) => {
const { id } = c.req.param()
try {
await equipmentService.deleteEquipment(id)
return c.json({ success: true })
} catch (e) {
console.error('Failed to delete equipment', e)
return c.json({ error: 'Failed to delete equipment' }, 500)
}
})
export default routeWithDelete
+36
View File
@@ -5,18 +5,35 @@ const mockPrisma = {
findMany: mock(),
count: mock(),
update: mock(),
delete: mock(),
findUnique: mock(),
},
generationEquipment: {
deleteMany: mock(),
},
}
const mockAssetService = {
deleteAsset: mock(),
}
mock.module('../db', () => ({
prisma: mockPrisma,
}))
mock.module('./asset', () => ({
assetService: mockAssetService,
}))
describe('EquipmentService', () => {
afterEach(() => {
mockPrisma.equipment.findMany.mockClear()
mockPrisma.equipment.count.mockClear()
mockPrisma.equipment.update.mockClear()
mockPrisma.equipment.delete.mockClear()
mockPrisma.equipment.findUnique.mockClear()
mockPrisma.generationEquipment.deleteMany.mockClear()
mockAssetService.deleteAsset.mockClear()
})
it('listEquipments should filter by category', async () => {
@@ -60,4 +77,23 @@ describe('EquipmentService', () => {
}),
)
})
it('deleteEquipment should delete equipment and associated asset', async () => {
// @ts-ignore
const { equipmentService } = await import(`./equipment?v=${Date.now()}`)
mockPrisma.equipment.findUnique.mockResolvedValue({
id: '1',
imageId: 'asset-1',
})
mockPrisma.generationEquipment.deleteMany.mockResolvedValue({ count: 1 })
mockPrisma.equipment.delete.mockResolvedValue({ id: '1' })
mockAssetService.deleteAsset.mockResolvedValue(undefined)
await equipmentService.deleteEquipment('1')
expect(mockPrisma.equipment.findUnique).toHaveBeenCalledWith({ where: { id: '1' } })
expect(mockPrisma.generationEquipment.deleteMany).toHaveBeenCalledWith({ where: { equipmentId: '1' } })
expect(mockPrisma.equipment.delete).toHaveBeenCalledWith({ where: { id: '1' } })
expect(mockAssetService.deleteAsset).toHaveBeenCalledWith('asset-1')
})
})
+34
View File
@@ -1,6 +1,7 @@
import { prisma } from '../db'
import { Prisma } from '../generated/prisma/client'
import { ulid } from 'ulidx'
import { assetService } from './asset'
export class EquipmentService {
/**
@@ -87,6 +88,39 @@ export class EquipmentService {
},
})
}
/**
* Deletes an equipment item and its associated asset.
*/
async deleteEquipment(id: string) {
const equipment = await prisma.equipment.findUnique({
where: { id },
})
if (!equipment) return
// Delete related GenerationEquipment records manually since there is no cascade
await prisma.generationEquipment.deleteMany({
where: { equipmentId: id },
})
// Delete equipment
await prisma.equipment.delete({
where: { id },
})
// Delete associated asset
// Note: We assume the asset is exclusive to this equipment, which is true for extracted assets.
if (equipment.imageId) {
try {
await assetService.deleteAsset(equipment.imageId)
} catch (e) {
console.warn(`Failed to delete asset ${equipment.imageId} for equipment ${id}`, e)
}
}
return equipment
}
}
export const equipmentService = new EquipmentService()
+24
View File
@@ -91,6 +91,22 @@ export default function Equipments() {
resetEquipment()
}, [selectedCategory, resetEquipment])
const handleDeleteEquipment = async (id: string) => {
if (!confirm('Are you sure you want to delete this equipment? This cannot be undone.')) return
try {
const res = await client.equipments[':id'].$delete({ param: { id } })
if (res.ok) {
toast.success('Equipment deleted')
resetEquipment()
} else {
toast.error('Failed to delete equipment')
}
} catch (e) {
console.error(e)
toast.error('Failed to delete equipment')
}
}
const handleDeleteOutfit = async (id: string) => {
if (!confirm('Are you sure you want to delete this outfit?')) return
try {
@@ -198,6 +214,14 @@ export default function Equipments() {
setSelectedEquipment(item)
setEquipmentDetailsOpen(true)
}}
actions={[
{
name: 'Delete',
onClick: () => handleDeleteEquipment(item.id),
variant: 'destructive',
icon: <Trash2 className="w-4 h-4" />,
},
]}
/>
))}
</div>