Skip to content

Commit 4f07558

Browse files
sdornanclaude
andcommitted
fix: refresh the v2 gallery when a ROM is edited or matched
The v2 gallery renders from `galleryRoms.byPosition`, but every v2 write site only patched v1's `stores/roms`, whose `_allRoms` the gallery never reads. Edits and matches replace the ROM with a fresh object from the API response, so the card kept showing the pre-edit name and cover until its window happened to be refetched. Optimistic toggles appeared to work only because they mutate the cached object in place, and the gallery holds that same reference. Add `useRomSync` and route the v2 write sites through it: - `syncRom` fans a write out to both stores. `galleryRoms.update` already existed for exactly this and had no callers. - `applyRomWrite` (edit / match dialogs) also refetches when an in-place swap would leave the list lying: any active filter, since a match rewrites provider ids, name and the metadata behind half the drawer, or a change to the value the gallery is currently ordered by. - `refreshAfterUserStateChange` covers favourite / status writes, guarded narrowly on the filters those can actually move. The existing "drop it from the Favourites view" branches check v1's collection context, which v2 never sets, so they were dead under v2. Optimistic toggles keep the cheap in-place update: invalidating the windows costs skeletons and the scroll position. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent a5854b5 commit 4f07558

15 files changed

Lines changed: 347 additions & 22 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useI18n } from "vue-i18n";
1010
import romApi from "@/services/api/rom";
1111
import storeRoms, { type DetailedRom } from "@/stores/roms";
1212
import type { Events } from "@/types/emitter";
13+
import { useRomSync } from "@/v2/composables/useRomSync";
1314
import { useSnackbar } from "@/v2/composables/useSnackbar";
1415
1516
defineOptions({ inheritAttrs: false });
@@ -18,6 +19,7 @@ const { t } = useI18n();
1819
const emitter = inject<Emitter<Events>>("emitter");
1920
const snackbar = useSnackbar();
2021
const romsStore = storeRoms();
22+
const { syncRom } = useRomSync();
2123
2224
const show = ref(false);
2325
const rom = ref<DetailedRom | null>(null);
@@ -48,7 +50,7 @@ async function refreshRom() {
4850
try {
4951
const { data } = await romApi.getRom({ romId: rom.value.id });
5052
romsStore.currentRom = data;
51-
romsStore.update(data);
53+
syncRom(data);
5254
} catch (error) {
5355
console.error(error);
5456
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import MetadataIdSection from "@/v2/components/EditRom/MetadataIdSection.vue";
3030
import RawMetadataPanel from "@/v2/components/EditRom/RawMetadataPanel.vue";
3131
import GameCard from "@/v2/components/GameCard/GameCard.vue";
3232
import { useBreakpoint } from "@/v2/composables/useBreakpoint";
33+
import { useRomSync } from "@/v2/composables/useRomSync";
3334
import { useSnackbar } from "@/v2/composables/useSnackbar";
3435
import { getMissingCoverImage } from "@/v2/utils/covers";
3536
@@ -59,6 +60,7 @@ const coverFileInput = ref<HTMLInputElement | null>(null);
5960
const saving = ref(false);
6061
const emitter = inject<Emitter<Events>>("emitter");
6162
const snackbar = useSnackbar();
63+
const { applyRomWrite } = useRomSync();
6264
6365
const openHandler = async (romToEdit: SimpleRom) => {
6466
show.value = true;
@@ -242,7 +244,7 @@ async function handleRomUpdate(
242244
try {
243245
const { data } = await romApi.updateRom(options);
244246
snackbar.success(successMessage, { icon: "mdi-check-bold" });
245-
romsStore.update(data as SimpleRom);
247+
applyRomWrite(data as SimpleRom);
246248
if (route.name === "rom") romsStore.currentRom = data;
247249
} catch (error: unknown) {
248250
console.error(error);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import romApi from "@/services/api/rom";
1212
import storeRoms, { type DetailedRom } from "@/stores/roms";
1313
import storeUpload from "@/stores/upload";
1414
import type { Events } from "@/types/emitter";
15+
import { useRomSync } from "@/v2/composables/useRomSync";
1516
import { useSnackbar } from "@/v2/composables/useSnackbar";
1617
1718
defineOptions({ inheritAttrs: false });
@@ -20,6 +21,7 @@ const { t } = useI18n();
2021
const emitter = inject<Emitter<Events>>("emitter");
2122
const snackbar = useSnackbar();
2223
const romsStore = storeRoms();
24+
const { syncRom } = useRomSync();
2325
const uploadStore = storeUpload();
2426
2527
const show = ref(false);
@@ -44,7 +46,7 @@ async function refreshRom() {
4446
try {
4547
const { data } = await romApi.getRom({ romId: rom.value.id });
4648
romsStore.currentRom = data;
47-
romsStore.update(data);
49+
syncRom(data);
4850
} catch (error) {
4951
console.error(error);
5052
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
MatchVariant,
3232
} from "@/v2/components/MatchRom/types";
3333
import { useBreakpoint } from "@/v2/composables/useBreakpoint";
34+
import { useRomSync } from "@/v2/composables/useRomSync";
3435
import { useSnackbar } from "@/v2/composables/useSnackbar";
3536
3637
defineOptions({ inheritAttrs: false });
@@ -71,6 +72,7 @@ const matchedRoms = ref<SearchRom[]>([]);
7172
const emitter = inject<Emitter<Events>>("emitter");
7273
const snackbar = useSnackbar();
7374
const heartbeat = storeHeartbeat();
75+
const { applyRomWrite } = useRomSync();
7476
7577
// Active body variant — the toolbar selector toggles between the
7678
// gallery-style grid and the master/detail list, mirroring the
@@ -257,7 +259,7 @@ async function onBodyConfirm(payload: ConfirmPayload) {
257259
snackbar.success(t("rom.rom-updated-successfully"), {
258260
icon: "mdi-check-bold",
259261
});
260-
romsStore.update(data as SimpleRom);
262+
applyRomWrite(data as SimpleRom);
261263
if (route.name === "rom") romsStore.currentRom = data;
262264
} catch (error: unknown) {
263265
const axiosErr = error as { response?: { data?: { detail?: string } } };

frontend/src/v2/components/Gallery/SelectionBar.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type { Events } from "@/types/emitter";
5353
import { romStatusMap } from "@/utils";
5454
import { useBreakpoint } from "@/v2/composables/useBreakpoint";
5555
import { useCan } from "@/v2/composables/useCan";
56+
import { useRomSync } from "@/v2/composables/useRomSync";
5657
import { useSnackbar } from "@/v2/composables/useSnackbar";
5758
import storeGallerySelection from "@/v2/stores/gallerySelection";
5859
import {
@@ -75,6 +76,7 @@ const snackbar = useSnackbar();
7576
const selection = storeGallerySelection();
7677
const collectionsStore = storeCollections();
7778
const romsStore = storeRoms();
79+
const { syncRom, refreshAfterUserStateChange } = useRomSync();
7880
7981
const canRefresh = useCan("rom.refresh");
8082
const canDownload = useCan("rom.download");
@@ -122,6 +124,8 @@ async function bulkFavorite() {
122124
// roms so the UI reflects the new membership immediately.
123125
romsStore.remove(selection.roms);
124126
}
127+
// The branch above only covers v1's gallery context; v2 tracks its own.
128+
refreshAfterUserStateChange();
125129
snackbar.success(
126130
allFavorited.value
127131
? t("gallery.selection-unfavorite-success", { n: ids.length })
@@ -178,7 +182,7 @@ async function applyStatus(data: Partial<RomUserData>) {
178182
if (!rom.rom_user) continue;
179183
before.set(rom.id, { ...rom.rom_user });
180184
Object.assign(rom.rom_user, data);
181-
romsStore.update(rom);
185+
syncRom(rom);
182186
}
183187
184188
const results = await Promise.allSettled(
@@ -189,10 +193,13 @@ async function applyStatus(data: Partial<RomUserData>) {
189193
const snapshot = before.get(rom.id);
190194
if (rom.rom_user && snapshot) {
191195
Object.assign(rom.rom_user, snapshot);
192-
romsStore.update(rom);
196+
syncRom(rom);
193197
}
194198
}
195199
200+
// Once for the whole batch, after the reverts are in.
201+
refreshAfterUserStateChange();
202+
196203
const ok = roms.length - failed.length;
197204
if (failed.length === 0) {
198205
snackbar.success(t("gallery.selection-status-success", { n: ok }));

frontend/src/v2/components/GameDetails/FilesTab/FilesTab.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import storeRoms from "@/stores/roms";
5353
import { getDownloadLink } from "@/utils";
5454
import { useCan } from "@/v2/composables/useCan";
5555
import { useConfirm } from "@/v2/composables/useConfirm";
56+
import { useRomSync } from "@/v2/composables/useRomSync";
5657
import { useSnackbar } from "@/v2/composables/useSnackbar";
5758
import FileRow from "./FileRow.vue";
5859
import FilesSummary from "./FilesSummary.vue";
@@ -67,6 +68,7 @@ const confirm = useConfirm();
6768
const route = useRoute();
6869
const router = useRouter();
6970
const romsStore = storeRoms();
71+
const { syncRom } = useRomSync();
7072
7173
const canUpload = useCan("rom.upload");
7274
const hasDeleteGrant = useCan("rom.delete");
@@ -578,7 +580,7 @@ async function refreshRom() {
578580
try {
579581
const { data } = await romApi.getRom({ romId: props.rom.id });
580582
romsStore.currentRom = data;
581-
romsStore.update(data);
583+
syncRom(data);
582584
} catch (error) {
583585
console.error(error);
584586
}

frontend/src/v2/components/GameDetails/MainSiblingToggle.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import { computed } from "vue";
1515
import { useI18n } from "vue-i18n";
1616
import type { RomUserData } from "@/__generated__";
1717
import romApi from "@/services/api/rom";
18-
import storeRoms from "@/stores/roms";
1918
import type { DetailedRom } from "@/stores/roms";
19+
import { useRomSync } from "@/v2/composables/useRomSync";
2020
import { useSnackbar } from "@/v2/composables/useSnackbar";
2121
2222
defineOptions({ inheritAttrs: false });
@@ -27,7 +27,7 @@ const props = defineProps<{
2727
2828
const { t } = useI18n();
2929
const snackbar = useSnackbar();
30-
const romsStore = storeRoms();
30+
const { syncRom } = useRomSync();
3131
3232
const visible = computed(
3333
() => props.rom.sibling_roms.length > 0 && props.rom.rom_user != null,
@@ -48,13 +48,13 @@ async function toggle() {
4848
const data: Partial<RomUserData> = { is_main_sibling: next };
4949
5050
ru.is_main_sibling = next;
51-
romsStore.update(props.rom);
51+
syncRom(props.rom);
5252
5353
try {
5454
await romApi.updateUserRomProps({ romId: props.rom.id, data });
5555
} catch {
5656
ru.is_main_sibling = before;
57-
romsStore.update(props.rom);
57+
syncRom(props.rom);
5858
snackbar.error(t("rom.update-default-failed"), {
5959
icon: "mdi-alert-circle-outline",
6060
});

frontend/src/v2/components/GameDetails/ManualSubtab.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Events } from "@/types/emitter";
1818
import { FRONTEND_RESOURCES_PATH } from "@/utils";
1919
import { useCan } from "@/v2/composables/useCan";
2020
import { useConfirm } from "@/v2/composables/useConfirm";
21+
import { useRomSync } from "@/v2/composables/useRomSync";
2122
import { useSnackbar } from "@/v2/composables/useSnackbar";
2223
2324
const PdfViewer = defineAsyncComponent(
@@ -41,6 +42,7 @@ const emitter = inject<Emitter<Events>>("emitter");
4142
const snackbar = useSnackbar();
4243
const confirm = useConfirm();
4344
const romsStore = storeRoms();
45+
const { syncRom } = useRomSync();
4446
const { t } = useI18n();
4547
4648
// Every manual endpoint (upload / redownload / delete) gates on the ROM write
@@ -141,7 +143,7 @@ async function refreshRom() {
141143
try {
142144
const { data } = await romApi.getRom({ romId: props.rom.id });
143145
romsStore.currentRom = data;
144-
romsStore.update(data);
146+
syncRom(data);
145147
} catch (error) {
146148
console.error(error);
147149
}

frontend/src/v2/components/GameDetails/MediaTab.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import storeRoms, { type DetailedRom } from "@/stores/roms";
2121
import storeUpload from "@/stores/upload";
2222
import { useCan } from "@/v2/composables/useCan";
2323
import { useConfirm } from "@/v2/composables/useConfirm";
24+
import { useRomSync } from "@/v2/composables/useRomSync";
2425
import { useSnackbar } from "@/v2/composables/useSnackbar";
2526
2627
const ManualSubtab = defineAsyncComponent(
@@ -49,6 +50,7 @@ const props = defineProps<{ rom: DetailedRom }>();
4950
const snackbar = useSnackbar();
5051
const confirm = useConfirm();
5152
const romsStore = storeRoms();
53+
const { syncRom } = useRomSync();
5254
const uploadStore = storeUpload();
5355
const { t } = useI18n();
5456
@@ -159,7 +161,7 @@ async function refreshRom() {
159161
try {
160162
const { data } = await romApi.getRom({ romId: props.rom.id });
161163
romsStore.currentRom = data;
162-
romsStore.update(data);
164+
syncRom(data);
163165
} catch (error) {
164166
console.error(error);
165167
}

frontend/src/v2/components/GameDetails/SaveDataTab.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import storeRoms from "@/stores/roms";
3636
import AssetList from "@/v2/components/shared/AssetList.vue";
3737
import AssetStrip from "@/v2/components/shared/AssetStrip.vue";
3838
import { useConfirm } from "@/v2/composables/useConfirm";
39+
import { useRomSync } from "@/v2/composables/useRomSync";
3940
import { useSnackbar } from "@/v2/composables/useSnackbar";
4041
4142
// Slot payload from AssetList/AssetStrip is the full save|state union; these
@@ -138,6 +139,7 @@ const uploadingStates = ref(false);
138139
const snackbar = useSnackbar();
139140
const confirm = useConfirm();
140141
const romsStore = storeRoms();
142+
const { syncRom } = useRomSync();
141143
142144
function errorMessage(err: unknown): string {
143145
if (axios.isAxiosError(err)) {
@@ -152,7 +154,7 @@ async function refreshRom() {
152154
try {
153155
const { data } = await romApi.getRom({ romId: props.rom.id });
154156
romsStore.currentRom = data;
155-
romsStore.update(data);
157+
syncRom(data);
156158
} catch (error) {
157159
console.error(error);
158160
}

0 commit comments

Comments
 (0)