mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
fix(settings): deleted API key stays on screen until manual reload
The delete-API-key mutation called queryClient.invalidateQueries on the ['api-keys'] cache, which in react-query v5 should also refetch active queries — but in practice the deleted row remained visible until the user reloaded the page. Switched onSuccess to setQueryData so the deleted key is filtered out of the cache synchronously the moment the API confirms; no refetch round-trip required, no invalidation→refetch race possible. Create-path is unchanged (invalidateQueries was working there). Pins the contract with a new SettingsPage test that mocks the GET/DELETE endpoints, clicks delete, confirms, and asserts the row is gone without any reload.
This commit is contained in:
@@ -422,4 +422,72 @@ describe('SettingsPage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Keys tab — delete flow', () => {
|
||||
// Without setQueryData on success the deleted row stayed visible until a
|
||||
// manual reload — invalidateQueries didn't reliably trigger a UI swap on
|
||||
// every browser. Pin the synchronous-removal contract here.
|
||||
it('removes a deleted key from the list without a page reload', async () => {
|
||||
const initialKeys = [
|
||||
{
|
||||
id: 42,
|
||||
name: 'CI deploy key',
|
||||
key_prefix: 'bk_abcd1234',
|
||||
can_queue: true,
|
||||
can_control_printer: false,
|
||||
can_read_status: true,
|
||||
printer_ids: null,
|
||||
enabled: true,
|
||||
last_used: null,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
expires_at: null,
|
||||
},
|
||||
];
|
||||
|
||||
let deleteCallCount = 0;
|
||||
server.use(
|
||||
http.get('/api/v1/api-keys/', () => HttpResponse.json(initialKeys)),
|
||||
http.delete('/api/v1/api-keys/:id', ({ params }) => {
|
||||
deleteCallCount += 1;
|
||||
expect(params.id).toBe('42');
|
||||
return HttpResponse.json({ message: 'API key deleted' });
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<SettingsPage />);
|
||||
|
||||
// Switch to API Keys tab. Both desktop tab + mobile dropdown render
|
||||
// the label, so just grab the button form.
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('API Keys').length).toBeGreaterThan(0);
|
||||
});
|
||||
const tabButton = screen.getAllByText('API Keys').find((el) => el.tagName === 'BUTTON');
|
||||
expect(tabButton).toBeDefined();
|
||||
await user.click(tabButton!);
|
||||
|
||||
// Key is listed
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('CI deploy key')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click the trash button on the row
|
||||
const cards = screen.getByText('CI deploy key').closest('.flex.items-center.justify-between');
|
||||
expect(cards).not.toBeNull();
|
||||
const trashButton = cards!.querySelectorAll('button');
|
||||
await user.click(trashButton[trashButton.length - 1]);
|
||||
|
||||
// Confirm the deletion in the modal
|
||||
const confirmButton = await screen.findByRole('button', { name: /delete/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
// The deleted key disappears from the list immediately — no manual
|
||||
// reload required. setQueryData drops it before any refetch could fire.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('CI deploy key')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(deleteCallCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api } from '../api/client';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { formatDateOnly } from '../utils/date';
|
||||
import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency';
|
||||
import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse } from '../api/client';
|
||||
import type { APIKey, AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse } from '../api/client';
|
||||
import { Card, CardContent, CardDensityProvider, CardHeader } from '../components/Card';
|
||||
import { CameraTokensSection } from './CameraTokensPage';
|
||||
import { Collapsible } from '../components/Collapsible';
|
||||
@@ -402,8 +402,10 @@ export function SettingsPage() {
|
||||
|
||||
const deleteAPIKeyMutation = useMutation({
|
||||
mutationFn: (id: number) => api.deleteAPIKey(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['api-keys'] });
|
||||
onSuccess: (_data, deletedId) => {
|
||||
queryClient.setQueryData<APIKey[]>(['api-keys'], (old) =>
|
||||
(old ?? []).filter((key) => key.id !== deletedId)
|
||||
);
|
||||
showToast(t('settings.toast.apiKeyDeleted'));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
|
||||
Reference in New Issue
Block a user