fix(admin): keep a parallel addon toggle when a photo provider toggle fails on the phone

The phone rolled a failed photo provider toggle back by restoring the whole addon list it saw at the click, which also undid any switch flipped while the request was in flight. Photo providers now go through the same toggle as every other addon, with a per row rollback, as on desktop.
This commit is contained in:
Maurice
2026-10-09 10:33:34 +02:00
parent 163b4c54e0
commit 2a242f29fb
2 changed files with 31 additions and 19 deletions
@@ -111,7 +111,8 @@ export default function MAdminAddonManager({ bagTrackingEnabled, onToggleBagTrac
const handleToggle = async (addon: Addon) => {
const newEnabled = !addon.enabled
// Optimistic update
// Optimistic flip, per-row rollback on failure. Rolling the whole addons snapshot
// back instead would undo a toggle the user hit in parallel.
setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: newEnabled } : a))
try {
await adminApi.updateAddon(addon.id, { enabled: newEnabled })
@@ -139,22 +140,6 @@ export default function MAdminAddonManager({ bagTrackingEnabled, onToggleBagTrac
return addon.type === 'trip' && (addon.icon === 'Image' || haystack.includes('photo') || haystack.includes('memories'))
}
const handleTogglePhotoProvider = async (providerAddon: Addon) => {
const enableProvider = !providerAddon.enabled
const prev = addons
setAddons(current => current.map(a => a.id === providerAddon.id ? { ...a, enabled: enableProvider } : a))
try {
await adminApi.updateAddon(providerAddon.id, { enabled: enableProvider })
refreshGlobalAddons()
toast.success(t('admin.addons.toast.updated'))
} catch {
setAddons(prev)
toast.error(t('admin.addons.toast.error'))
}
}
const photoProviderAddons = addons.filter(isPhotoProviderAddon)
const documentProviderAddons = addons.filter(a => a.type === 'document_provider')
const tripAddons = addons.filter(a => a.type === 'trip' && !isPhotosAddon(a))
@@ -165,7 +150,7 @@ export default function MAdminAddonManager({ bagTrackingEnabled, onToggleBagTrac
label: provider.name,
description: provider.description,
enabled: provider.enabled,
toggle: () => handleTogglePhotoProvider(provider),
toggle: () => handleToggle(provider),
}))
// No credential form under these, unlike the photo providers: a document
// connection belongs to a trip and is entered there. The admin only decides
@@ -1,4 +1,4 @@
// FE-MOB-AADD-001 to FE-MOB-AADD-033
// FE-MOB-AADD-001 to FE-MOB-AADD-034
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import userEvent from '@testing-library/user-event';
import { delay, http, HttpResponse } from 'msw';
@@ -316,6 +316,33 @@ describe('MAdminAddonManager', () => {
await waitFor(() => expect(screen.getByRole('switch', { name: 'Unsplash' })).toHaveAttribute('aria-checked', 'false'));
});
it('FE-MOB-AADD-034: a failing provider toggle keeps a provider toggled while it was in flight', async () => {
const user = userEvent.setup();
server.use(
addonsRoute([
buildAddon({ id: 'journey', name: 'Journey', type: 'global', icon: 'Compass', enabled: true }),
buildAddon({ id: 'immich', name: 'Immich', description: 'Self-hosted photos', type: 'photo_provider', enabled: false }),
buildAddon({ id: 'unsplash', name: 'Unsplash', description: 'Stock photos', type: 'photo_provider', enabled: false }),
]),
http.put('/api/admin/addons/immich', () => HttpResponse.json({ success: true })),
http.put('/api/admin/addons/unsplash', async () => {
await delay(150);
return HttpResponse.error();
}),
);
render(<><ToastContainer /><MAdminAddonManager /></>);
await screen.findByText('Immich');
// Unsplash is still saving when Immich goes on and through.
await user.click(screen.getByRole('switch', { name: 'Unsplash' }));
await user.click(screen.getByRole('switch', { name: 'Immich' }));
await screen.findByText('Addon updated');
await screen.findByText('Failed to update addon');
await waitFor(() => expect(screen.getByRole('switch', { name: 'Unsplash' })).toHaveAttribute('aria-checked', 'false'));
expect(screen.getByRole('switch', { name: 'Immich' })).toHaveAttribute('aria-checked', 'true');
});
it('FE-MOB-AADD-026: switching the journey addon off and on again shows the cascaded providers as off', async () => {
const user = userEvent.setup();
let loads = 0;