Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(admin): add success toast notifications to all save actions (#143)

Admin mutations showed error feedback on failure but no confirmation on
success. Wire useToast() into all 8 admin pages/hooks and fire a toast
after each successful mutation (24 total). Add useToast mock to the
corresponding 8 test files.

authored by

Guido X Jansen and committed by
GitHub
b584a3fd 927d7eed

+69
+4
src/app/admin/categories/page.test.tsx
··· 51 51 return { useAuth: () => mockAuth } 52 52 }) 53 53 54 + vi.mock('@/hooks/use-toast', () => ({ 55 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 56 + })) 57 + 54 58 describe('AdminCategoriesPage', () => { 55 59 it('renders categories heading', () => { 56 60 render(<AdminCategoriesPage />)
+4
src/app/admin/categories/page.tsx
··· 17 17 import { getCategories, createCategory, updateCategory, deleteCategory } from '@/lib/api/client' 18 18 import type { CategoryTreeNode } from '@/lib/api/types' 19 19 import { useAuth } from '@/hooks/use-auth' 20 + import { useToast } from '@/hooks/use-toast' 20 21 21 22 export default function AdminCategoriesPage() { 22 23 const { getAccessToken } = useAuth() 24 + const { toast } = useToast() 23 25 const [categories, setCategories] = useState<CategoryTreeNode[]>([]) 24 26 const [loading, setLoading] = useState(true) 25 27 const [editing, setEditing] = useState<EditingCategory | null>(null) ··· 69 71 try { 70 72 await deleteCategory(id, getAccessToken() ?? '') 71 73 void fetchCategories() 74 + toast({ title: 'Category deleted' }) 72 75 } catch { 73 76 setActionError('Failed to delete category. Please try again.') 74 77 } ··· 105 108 } 106 109 setEditing(null) 107 110 void fetchCategories() 111 + toast({ title: editing.id ? 'Category updated' : 'Category created' }) 108 112 } catch { 109 113 setActionError('Failed to save category. Please try again.') 110 114 }
+4
src/app/admin/moderation/page.test.tsx
··· 51 51 return { useAuth: () => mockAuth } 52 52 }) 53 53 54 + vi.mock('@/hooks/use-toast', () => ({ 55 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 56 + })) 57 + 54 58 describe('AdminModerationPage', () => { 55 59 it('renders moderation heading', () => { 56 60 render(<AdminModerationPage />)
+4
src/app/admin/onboarding/page.test.tsx
··· 51 51 return { useAuth: () => mockAuth } 52 52 }) 53 53 54 + vi.mock('@/hooks/use-toast', () => ({ 55 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 56 + })) 57 + 54 58 describe('AdminOnboardingPage', () => { 55 59 it('renders onboarding fields heading', () => { 56 60 render(<AdminOnboardingPage />)
+4
src/app/admin/plugins/page.test.tsx
··· 33 33 return { useAuth: () => mockAuth } 34 34 }) 35 35 36 + vi.mock('@/hooks/use-toast', () => ({ 37 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 38 + })) 39 + 36 40 describe('AdminPluginsPage', () => { 37 41 it('renders page heading', async () => { 38 42 render(<AdminPluginsPage />)
+4
src/app/admin/settings/page.test.tsx
··· 51 51 return { useAuth: () => mockAuth } 52 52 }) 53 53 54 + vi.mock('@/hooks/use-toast', () => ({ 55 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 56 + })) 57 + 54 58 describe('AdminSettingsPage', () => { 55 59 it('renders community settings heading', () => { 56 60 render(<AdminSettingsPage />)
+4
src/app/admin/settings/page.tsx
··· 20 20 } from '@/lib/api/client' 21 21 import type { CommunitySettings, PdsTrustFactor } from '@/lib/api/types' 22 22 import { useAuth } from '@/hooks/use-auth' 23 + import { useToast } from '@/hooks/use-toast' 23 24 24 25 export default function AdminSettingsPage() { 25 26 const { getAccessToken } = useAuth() 27 + const { toast } = useToast() 26 28 const [settings, setSettings] = useState<CommunitySettings | null>(null) 27 29 const [pdsProviders, setPdsProviders] = useState<PdsTrustFactor[]>([]) 28 30 const [loading, setLoading] = useState(true) ··· 68 70 getAccessToken() ?? '' 69 71 ) 70 72 setSettings(updated) 73 + toast({ title: 'Settings saved' }) 71 74 } catch { 72 75 setSaveError('Failed to save settings. Please try again.') 73 76 } finally { ··· 86 89 } 87 90 return [...prev, updated] 88 91 }) 92 + toast({ title: 'PDS trust factor updated' }) 89 93 } catch { 90 94 setPdsError('Failed to update PDS trust factor.') 91 95 }
+4
src/app/admin/sybil-detection/page.test.tsx
··· 52 52 return { useAuth: () => mockAuth } 53 53 }) 54 54 55 + vi.mock('@/hooks/use-toast', () => ({ 56 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 57 + })) 58 + 55 59 describe('AdminSybilDetectionPage', () => { 56 60 it('renders heading and explanation text', async () => { 57 61 render(<AdminSybilDetectionPage />)
+4
src/app/admin/trust-seeds/page.test.tsx
··· 52 52 return { useAuth: () => mockAuth } 53 53 }) 54 54 55 + vi.mock('@/hooks/use-toast', () => ({ 56 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 57 + })) 58 + 55 59 describe('AdminTrustSeedsPage', () => { 56 60 it('renders heading and help text', async () => { 57 61 render(<AdminTrustSeedsPage />)
+4
src/app/admin/trust-seeds/page.tsx
··· 16 16 import { getTrustSeeds, createTrustSeed, deleteTrustSeed } from '@/lib/api/client' 17 17 import type { TrustSeed } from '@/lib/api/types' 18 18 import { useAuth } from '@/hooks/use-auth' 19 + import { useToast } from '@/hooks/use-toast' 19 20 20 21 export default function AdminTrustSeedsPage() { 21 22 const { getAccessToken } = useAuth() 23 + const { toast } = useToast() 22 24 const [seeds, setSeeds] = useState<TrustSeed[]>([]) 23 25 const [loading, setLoading] = useState(true) 24 26 const [loadError, setLoadError] = useState<string | null>(null) ··· 54 56 const newSeed = await createTrustSeed(data, getAccessToken() ?? '') 55 57 setSeeds((prev) => [...prev, newSeed]) 56 58 setAddDialogOpen(false) 59 + toast({ title: 'Trust seed added' }) 57 60 } catch { 58 61 setActionError('Failed to add trust seed.') 59 62 } ··· 69 72 try { 70 73 await deleteTrustSeed(seed.id, getAccessToken() ?? '') 71 74 setSeeds((prev) => prev.filter((s) => s.id !== seed.id)) 75 + toast({ title: 'Trust seed removed' }) 72 76 } catch { 73 77 setActionError('Failed to remove trust seed.') 74 78 }
+4
src/app/admin/users/page.test.tsx
··· 50 50 return { useAuth: () => mockAuth } 51 51 }) 52 52 53 + vi.mock('@/hooks/use-toast', () => ({ 54 + useToast: () => ({ toast: vi.fn(), dismiss: vi.fn() }), 55 + })) 56 + 53 57 describe('AdminUsersPage', () => { 54 58 it('renders user management heading', () => { 55 59 render(<AdminUsersPage />)
+4
src/app/admin/users/page.tsx
··· 14 14 import { getAdminUsers, banUser, unbanUser } from '@/lib/api/client' 15 15 import type { AdminUser } from '@/lib/api/types' 16 16 import { useAuth } from '@/hooks/use-auth' 17 + import { useToast } from '@/hooks/use-toast' 17 18 18 19 export default function AdminUsersPage() { 19 20 const { getAccessToken } = useAuth() 21 + const { toast } = useToast() 20 22 const [users, setUsers] = useState<AdminUser[]>([]) 21 23 const [loading, setLoading] = useState(true) 22 24 const [loadError, setLoadError] = useState<string | null>(null) ··· 54 56 : u 55 57 ) 56 58 ) 59 + toast({ title: 'User banned' }) 57 60 } catch { 58 61 setActionError('Failed to ban user. Please try again.') 59 62 } ··· 68 71 u.did === did ? { ...u, isBanned: false, bannedAt: null, banReason: null } : u 69 72 ) 70 73 ) 74 + toast({ title: 'User unbanned' }) 71 75 } catch { 72 76 setActionError('Failed to unban user. Please try again.') 73 77 }
+6
src/hooks/admin/use-moderation-data.ts
··· 25 25 ReportResolution, 26 26 } from '@/lib/api/types' 27 27 import { useAuth } from '@/hooks/use-auth' 28 + import { useToast } from '@/hooks/use-toast' 28 29 29 30 export type ModerationTabId = 30 31 | 'reports' ··· 43 44 44 45 export function useModerationData() { 45 46 const { getAccessToken } = useAuth() 47 + const { toast } = useToast() 46 48 const [activeTab, setActiveTab] = useState<ModerationTabId>('reports') 47 49 const [reports, setReports] = useState<ModerationReport[]>([]) 48 50 const [firstPostQueue, setFirstPostQueue] = useState<FirstPostQueueItem[]>([]) ··· 84 86 try { 85 87 await resolveReport(id, resolution, getAccessToken() ?? '') 86 88 setReports((prev) => prev.filter((r) => r.id !== id)) 89 + toast({ title: 'Report resolved' }) 87 90 } catch { 88 91 setActionError('Failed to resolve report. Please try again.') 89 92 } ··· 94 97 try { 95 98 await resolveFirstPost(id, action, getAccessToken() ?? '') 96 99 setFirstPostQueue((prev) => prev.filter((item) => item.id !== id)) 100 + toast({ title: action === 'approved' ? 'Post approved' : 'Post rejected' }) 97 101 } catch { 98 102 setActionError( 99 103 `Failed to ${action === 'approved' ? 'approve' : 'reject'} post. Please try again.` ··· 106 110 try { 107 111 await Promise.all(ids.map((id) => resolveFirstPost(id, action, getAccessToken() ?? ''))) 108 112 setFirstPostQueue((prev) => prev.filter((item) => !ids.includes(item.id))) 113 + toast({ title: action === 'approved' ? 'Posts approved' : 'Posts rejected' }) 109 114 } catch { 110 115 setActionError('Failed to process batch action. Some items may not have been updated.') 111 116 } ··· 116 121 try { 117 122 const result = await updateModerationThresholds(updated, getAccessToken() ?? '') 118 123 setThresholds(result) 124 + toast({ title: 'Thresholds saved' }) 119 125 } catch { 120 126 setActionError('Failed to save thresholds. Please try again.') 121 127 }
+4
src/hooks/admin/use-onboarding-fields.ts
··· 16 16 import { EMPTY_FIELD } from '@/components/admin/onboarding/onboarding-field-form' 17 17 import type { EditingField } from '@/components/admin/onboarding/onboarding-field-form' 18 18 import { useAuth } from '@/hooks/use-auth' 19 + import { useToast } from '@/hooks/use-toast' 19 20 20 21 export function useOnboardingFields() { 21 22 const { getAccessToken } = useAuth() 23 + const { toast } = useToast() 22 24 const [fields, setFields] = useState<OnboardingField[]>([]) 23 25 const [loading, setLoading] = useState(true) 24 26 const [editing, setEditing] = useState<EditingField | null>(null) ··· 65 67 try { 66 68 await deleteOnboardingField(id, getAccessToken() ?? '') 67 69 void fetchFields() 70 + toast({ title: 'Field deleted' }) 68 71 } catch { 69 72 setActionError('Failed to delete field. Please try again.') 70 73 } ··· 104 107 } 105 108 setEditing(null) 106 109 void fetchFields() 110 + toast({ title: editing.id ? 'Field updated' : 'Field created' }) 107 111 } catch { 108 112 setError('Failed to save field') 109 113 } finally {
+6
src/hooks/admin/use-plugin-management.ts
··· 9 9 import { getPlugins, togglePlugin, updatePluginSettings, uninstallPlugin } from '@/lib/api/client' 10 10 import type { Plugin } from '@/lib/api/types' 11 11 import { useAuth } from '@/hooks/use-auth' 12 + import { useToast } from '@/hooks/use-toast' 12 13 13 14 interface DependencyWarning { 14 15 plugin: Plugin ··· 17 18 18 19 export function usePluginManagement() { 19 20 const { getAccessToken } = useAuth() 21 + const { toast } = useToast() 20 22 const [plugins, setPlugins] = useState<Plugin[]>([]) 21 23 const [loading, setLoading] = useState(true) 22 24 const [settingsPlugin, setSettingsPlugin] = useState<Plugin | null>(null) ··· 60 62 setPlugins((prev) => 61 63 prev.map((p) => (p.id === plugin.id ? { ...p, enabled: !p.enabled } : p)) 62 64 ) 65 + toast({ title: plugin.enabled ? 'Plugin disabled' : 'Plugin enabled' }) 63 66 } catch { 64 67 setActionError(`Failed to ${plugin.enabled ? 'disable' : 'enable'} plugin. Please try again.`) 65 68 } ··· 73 76 setPlugins((prev) => 74 77 prev.map((p) => (p.id === dependencyWarning.plugin.id ? { ...p, enabled: false } : p)) 75 78 ) 79 + toast({ title: 'Plugin disabled' }) 76 80 } catch { 77 81 setActionError('Failed to disable plugin. Please try again.') 78 82 } ··· 85 89 try { 86 90 await updatePluginSettings(settingsPlugin.id, settings, getAccessToken() ?? '') 87 91 setPlugins((prev) => prev.map((p) => (p.id === settingsPlugin.id ? { ...p, settings } : p))) 92 + toast({ title: 'Plugin settings saved' }) 88 93 } catch { 89 94 setActionError('Failed to save plugin settings. Please try again.') 90 95 } ··· 96 101 try { 97 102 await uninstallPlugin(plugin.id, getAccessToken() ?? '') 98 103 setPlugins((prev) => prev.filter((p) => p.id !== plugin.id)) 104 + toast({ title: 'Plugin uninstalled' }) 99 105 } catch { 100 106 setActionError('Failed to uninstall plugin. Please try again.') 101 107 }
+5
src/hooks/admin/use-sybil-data.ts
··· 23 23 BehavioralFlag, 24 24 } from '@/lib/api/types' 25 25 import { useAuth } from '@/hooks/use-auth' 26 + import { useToast } from '@/hooks/use-toast' 26 27 27 28 export function useSybilData() { 28 29 const { getAccessToken } = useAuth() 30 + const { toast } = useToast() 29 31 const [clusters, setClusters] = useState<SybilCluster[]>([]) 30 32 const [graphStatus, setGraphStatus] = useState<TrustGraphStatus | null>(null) 31 33 const [flags, setFlags] = useState<BehavioralFlag[]>([]) ··· 94 96 ) 95 97 setClusters((prev) => prev.map((c) => (c.id === updated.id ? updated : c))) 96 98 setSelectedDetail({ ...selectedDetail, ...updated }) 99 + toast({ title: 'Cluster status updated' }) 97 100 } catch { 98 101 setActionError('Failed to update cluster status.') 99 102 } ··· 105 108 setRecomputing(true) 106 109 try { 107 110 await recomputeTrustGraph(getAccessToken() ?? '') 111 + toast({ title: 'Trust graph recomputation started' }) 108 112 } catch { 109 113 setActionError('Failed to start recomputation.') 110 114 } finally { ··· 117 121 try { 118 122 const updated = await updateBehavioralFlag(id, 'dismissed', getAccessToken() ?? '') 119 123 setFlags((prev) => prev.map((f) => (f.id === updated.id ? updated : f))) 124 + toast({ title: 'Flag dismissed' }) 120 125 } catch { 121 126 setActionError('Failed to dismiss flag.') 122 127 }