Skip to content

Commit 791ebd9

Browse files
authored
Merge pull request #3828 from rommapp/fix/v2-clear-selection-after-delete
fix(v2): clear gallery selection after deleting ROMs
2 parents 677937e + 5c6fc37 commit 791ebd9

6 files changed

Lines changed: 62 additions & 26 deletions

File tree

backend/endpoints/firmware.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,14 @@ async def delete_firmware(
284284
assert_can(perms, PermEntity.FIRMWARE, PermAction.DELETE)
285285

286286
successful_items = 0
287-
failed_items = 0
287+
failed_ids = []
288288
errors = []
289289

290290
for id in firmware:
291291
fw = db_firmware_handler.get_firmware(id)
292292
# Treat firmware on a hidden platform as non-existent for this caller.
293293
if not fw or not perms.can_see_platform(fw.platform_id):
294-
failed_items += 1
294+
failed_ids.append(id)
295295
errors.append(f"Firmware with ID {id} not found")
296296
continue
297297

@@ -308,16 +308,16 @@ async def delete_firmware(
308308
error = f"Firmware file {hl(fw.file_name)} not found for platform {hl(fw.platform.slug)}"
309309
log.error(error)
310310
errors.append(error)
311-
failed_items += 1
311+
failed_ids.append(id)
312312
continue
313313

314314
successful_items += 1
315315
except Exception as e:
316-
failed_items += 1
316+
failed_ids.append(id)
317317
errors.append(f"Failed to delete firmware {id}: {str(e)}")
318318

319319
return {
320320
"successful_items": successful_items,
321-
"failed_items": failed_items,
321+
"failed_ids": failed_ids,
322322
"errors": errors,
323323
}

backend/endpoints/responses/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,5 @@ class GenericTaskStatusResponse(BaseTaskStatusResponse):
149149

150150
class BulkOperationResponse(TypedDict):
151151
successful_items: int
152-
failed_items: int
152+
failed_ids: list[int]
153153
errors: list[str]

backend/endpoints/roms/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,15 +1896,15 @@ async def delete_roms(
18961896
assert_can(perms, PermEntity.ROMS, PermAction.DELETE)
18971897

18981898
successful_items = 0
1899-
failed_items = 0
1899+
failed_ids = []
19001900
errors = []
19011901

19021902
for id in roms:
19031903
rom = db_rom_handler.get_rom(id)
19041904

19051905
# Hidden roms are masked as not-found rather than reported deletable.
19061906
if not rom or not perms.can_see_rom(rom.id, rom.platform_id):
1907-
failed_items += 1
1907+
failed_ids.append(id)
19081908
errors.append(f"ROM with ID {id} not found")
19091909
continue
19101910

@@ -1952,15 +1952,15 @@ async def delete_roms(
19521952

19531953
successful_items += 1
19541954
except Exception as e:
1955-
failed_items += 1
1955+
failed_ids.append(id)
19561956
errors.append(f"Failed to delete ROM {id}: {str(e)}")
19571957

19581958
if successful_items:
19591959
db_rom_handler.invalidate_filter_values_cache()
19601960

19611961
return {
19621962
"successful_items": successful_items,
1963-
"failed_items": failed_items,
1963+
"failed_ids": failed_ids,
19641964
"errors": errors,
19651965
}
19661966

backend/tests/endpoints/roms/test_rom.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,23 @@ def test_delete_roms(client: TestClient, access_token: str, rom: Rom):
570570
assert body["successful_items"] == 1
571571

572572

573+
def test_delete_roms_reports_failed_ids(
574+
client: TestClient, access_token: str, rom: Rom
575+
):
576+
missing_id = rom.id + 999999
577+
response = client.post(
578+
"/api/roms/delete",
579+
headers={"Authorization": f"Bearer {access_token}"},
580+
json={"roms": [rom.id, missing_id], "delete_from_fs": []},
581+
)
582+
assert response.status_code == status.HTTP_200_OK
583+
584+
body = response.json()
585+
assert body["successful_items"] == 1
586+
# The failed id stays reported so the client can keep it selected for retry.
587+
assert body["failed_ids"] == [missing_id]
588+
589+
573590
@patch(
574591
"endpoints.roms.fs_rom_handler.remove_directory",
575592
new_callable=AsyncMock,
@@ -607,7 +624,7 @@ def test_delete_roms_from_fs_flat(
607624

608625
body = response.json()
609626
assert body["successful_items"] == 1
610-
assert body["failed_items"] == 0
627+
assert body["failed_ids"] == []
611628
mock_remove_file.assert_called_once()
612629
mock_remove_directory.assert_not_called()
613630

@@ -651,7 +668,7 @@ def test_delete_roms_from_fs_flat_cleans_empty_parent(
651668

652669
body = response.json()
653670
assert body["successful_items"] == 1
654-
assert body["failed_items"] == 0
671+
assert body["failed_ids"] == []
655672
mock_remove_file.assert_called_once()
656673
# remove_directory should be called to clean up the empty parent dir
657674
mock_remove_directory.assert_called_once()
@@ -703,7 +720,7 @@ def test_delete_roms_from_fs_nested(
703720

704721
body = response.json()
705722
assert body["successful_items"] == 1
706-
assert body["failed_items"] == 0
723+
assert body["failed_ids"] == []
707724
mock_remove_directory.assert_called_once()
708725

709726

@@ -728,7 +745,7 @@ def test_delete_roms_from_fs_missing_file_still_deletes_db_entry(
728745

729746
body = response.json()
730747
assert body["successful_items"] == 1
731-
assert body["failed_items"] == 0
748+
assert body["failed_ids"] == []
732749
assert body["errors"] == []
733750
assert db_rom_handler.get_rom(rom.id) is None
734751

frontend/src/__generated__/models/BulkOperationResponse.ts

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/v2/components/Dialogs/DeleteRomDialog.vue

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import storeRoms, { type SimpleRom } from "@/stores/roms";
1515
import type { Events } from "@/types/emitter";
1616
import { useSnackbar } from "@/v2/composables/useSnackbar";
1717
import storeGalleryRoms from "@/v2/stores/galleryRoms";
18+
import storeGallerySelection from "@/v2/stores/gallerySelection";
1819
1920
defineOptions({ inheritAttrs: false });
2021
@@ -24,6 +25,7 @@ const route = useRoute();
2425
const show = ref(false);
2526
const romsStore = storeRoms();
2627
const galleryRomsStore = storeGalleryRoms();
28+
const gallerySelectionStore = storeGallerySelection();
2729
const roms = ref<SimpleRom[]>([]);
2830
const romsToDeleteFromFs = ref<number[]>([]);
2931
const excludeOnDelete = ref(false);
@@ -73,13 +75,27 @@ async function deleteRoms() {
7375
if (deleting.value) return;
7476
deleting.value = true;
7577
78+
// Snapshot the dialog state up front: the dialog is a singleton, so a
79+
// fresh `showDeleteRomDialog` event could replace these refs while the
80+
// request is in flight. Acting on the snapshot keeps the response tied
81+
// to the ROMs it actually processed.
82+
const targetRoms = roms.value;
83+
const targetPlatformId = platformId.value;
84+
const deleteFromFs = romsToDeleteFromFs.value;
85+
const exclude = excludeOnDelete.value;
86+
7687
try {
7788
const response = await romApi.deleteRoms({
78-
roms: roms.value,
79-
deleteFromFs: romsToDeleteFromFs.value,
89+
roms: targetRoms,
90+
deleteFromFs,
8091
});
92+
// The backend deletes per-ROM and can partially fail; only prune the
93+
// ROMs it actually removed so a failed subset stays visible and
94+
// selected for the user to retry.
95+
const failedIds = new Set(response.data.failed_ids);
96+
const deletedRoms = targetRoms.filter((rom) => !failedIds.has(rom.id));
8197
snackbar.success(
82-
fsCount.value > 0
98+
deleteFromFs.length > 0
8399
? t("rom.deleted-from-filesystem", {
84100
count: response.data.successful_items,
85101
})
@@ -88,8 +104,8 @@ async function deleteRoms() {
88104
}),
89105
{ icon: "mdi-check-bold" },
90106
);
91-
if (excludeOnDelete.value) {
92-
for (const rom of roms.value) {
107+
if (exclude) {
108+
for (const rom of deletedRoms) {
93109
const type = rom.has_simple_single_file
94110
? "EXCLUDED_SINGLE_FILES"
95111
: "EXCLUDED_MULTI_FILES";
@@ -101,24 +117,27 @@ async function deleteRoms() {
101117
}
102118
}
103119
romsStore.resetSelection();
104-
romsStore.remove(roms.value);
105-
galleryRomsStore.remove(roms.value);
120+
// Drop the deleted ROMs from the gallery selection
121+
gallerySelectionStore.removeIds(deletedRoms.map((rom) => rom.id));
122+
romsStore.remove(deletedRoms);
123+
galleryRomsStore.remove(deletedRoms);
106124
romsStore.setRecentRoms(
107125
romsStore.recentRoms.filter(
108-
(r) => !roms.value.some((rom) => rom.id === r.id),
126+
(r) => !deletedRoms.some((rom) => rom.id === r.id),
109127
),
110128
);
111129
romsStore.setContinuePlayingRoms(
112130
romsStore.continuePlayingRoms.filter(
113-
(r) => !roms.value.some((rom) => rom.id === r.id),
131+
(r) => !deletedRoms.some((rom) => rom.id === r.id),
114132
),
115133
);
116134
emitter?.emit("refreshDrawer", null);
117135
closeDialog();
118-
if (route.name === "rom") {
136+
// Only leave the single-ROM route when that ROM was actually deleted.
137+
if (route.name === "rom" && deletedRoms.length > 0) {
119138
router.push({
120139
name: ROUTES.PLATFORM,
121-
params: { platform: platformId.value },
140+
params: { platform: targetPlatformId },
122141
});
123142
}
124143
} catch (error: unknown) {

0 commit comments

Comments
 (0)