feat(pending): full-screen modal with grid cards, filters, bulk restore
Replaces sidebar pending/hidden panels with a wide modal showing devices as cards. Adds search, segmented source/status filters, type filter, select mode for bulk approve/hide/restore, and keyboard shortcuts. Hidden cards click-to-restore (no approval detour); approval no longer pops the edit modal. Backend: new restore + bulk-restore endpoints (hidden -> pending).
This commit is contained in:
@@ -174,6 +174,41 @@ async def bulk_hide_devices(
|
||||
return {"hidden": len(devices), "skipped": len(payload.device_ids) - len(devices)}
|
||||
|
||||
|
||||
@router.post("/pending/{device_id}/restore", response_model=dict)
|
||||
async def restore_device(
|
||||
device_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
device = await db.get(PendingDevice, device_id)
|
||||
if not device:
|
||||
raise HTTPException(status_code=404, detail="Device not found")
|
||||
if device.status != "hidden":
|
||||
raise HTTPException(status_code=409, detail="Device is not hidden")
|
||||
device.status = "pending"
|
||||
await db.commit()
|
||||
return {"restored": True, "device_id": device_id}
|
||||
|
||||
|
||||
@router.post("/pending/bulk-restore", response_model=dict)
|
||||
async def bulk_restore_devices(
|
||||
payload: BulkActionRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
result = await db.execute(
|
||||
select(PendingDevice).where(
|
||||
PendingDevice.id.in_(payload.device_ids),
|
||||
PendingDevice.status == "hidden",
|
||||
)
|
||||
)
|
||||
devices = result.scalars().all()
|
||||
for device in devices:
|
||||
device.status = "pending"
|
||||
await db.commit()
|
||||
return {"restored": len(devices), "skipped": len(payload.device_ids) - len(devices)}
|
||||
|
||||
|
||||
@router.post("/pending/{device_id}/approve", response_model=dict)
|
||||
async def approve_device(
|
||||
device_id: str,
|
||||
|
||||
@@ -140,6 +140,49 @@ async def test_hide_device(client: AsyncClient, headers, pending_device):
|
||||
assert len(hidden_res.json()) == 1
|
||||
|
||||
|
||||
# --- Restore hidden device ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_device(client: AsyncClient, headers, pending_device):
|
||||
# Hide first
|
||||
await client.post(f"/api/v1/scan/pending/{pending_device.id}/hide", headers=headers)
|
||||
|
||||
# Restore
|
||||
res = await client.post(f"/api/v1/scan/pending/{pending_device.id}/restore", headers=headers)
|
||||
assert res.status_code == 200
|
||||
assert res.json()["restored"] is True
|
||||
|
||||
# Now back in pending, gone from hidden
|
||||
pending_res = await client.get("/api/v1/scan/pending", headers=headers)
|
||||
assert len(pending_res.json()) == 1
|
||||
hidden_res = await client.get("/api/v1/scan/hidden", headers=headers)
|
||||
assert hidden_res.json() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restore_device_rejects_non_hidden(client: AsyncClient, headers, pending_device):
|
||||
res = await client.post(f"/api/v1/scan/pending/{pending_device.id}/restore", headers=headers)
|
||||
assert res.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bulk_restore_devices(client: AsyncClient, headers, pending_device):
|
||||
# Hide
|
||||
await client.post(f"/api/v1/scan/pending/{pending_device.id}/hide", headers=headers)
|
||||
|
||||
res = await client.post(
|
||||
"/api/v1/scan/pending/bulk-restore",
|
||||
headers=headers,
|
||||
json={"device_ids": [pending_device.id]},
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json()["restored"] == 1
|
||||
assert res.json()["skipped"] == 0
|
||||
|
||||
pending_res = await client.get("/api/v1/scan/pending", headers=headers)
|
||||
assert len(pending_res.json()) == 1
|
||||
|
||||
|
||||
# --- Ignore device ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user