From f4eb3fe186374d52a1f2d17d0df3045973d7f515 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sun, 8 Feb 2026 06:53:21 -0500 Subject: [PATCH] fix: resolve admin panel API path issues and ActionMenu overflow - Fix duplicate /api/v1 paths in admin API calls - Fix ActionMenu dropdown being clipped by using React Portal - Fix TeamCategoriesPage API endpoints Co-Authored-By: Claude Sonnet 4.5 --- frontend/src/api/admin.ts | 68 +++++++++---------- frontend/src/components/admin/ActionMenu.tsx | 47 ++++++++++--- .../src/pages/account/TeamCategoriesPage.tsx | 8 +-- 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 33444648..37f3a466 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -18,91 +18,91 @@ import type { export const adminApi = { // Dashboard getDashboardMetrics: () => - api.get('/api/v1/admin/dashboard/metrics').then(r => r.data), + api.get('/admin/dashboard/metrics').then(r => r.data), getDashboardActivity: () => - api.get('/api/v1/admin/dashboard/activity').then(r => r.data), + api.get('/admin/dashboard/activity').then(r => r.data), // Users (existing endpoints) listUsers: (params?: Record) => - api.get('/api/v1/admin/users', { params }).then(r => r.data), + api.get('/admin/users', { params }).then(r => r.data), getUser: (id: string) => - api.get(`/api/v1/admin/users/${id}`).then(r => r.data), + api.get(`/admin/users/${id}`).then(r => r.data), updateUserRole: (id: string, role: string) => - api.put(`/api/v1/admin/users/${id}/role`, { role }).then(r => r.data), + api.put(`/admin/users/${id}/role`, { role }).then(r => r.data), updateAccountRole: (id: string, account_role: string) => - api.put(`/api/v1/admin/users/${id}/account-role`, { account_role }).then(r => r.data), + api.put(`/admin/users/${id}/account-role`, { account_role }).then(r => r.data), deactivateUser: (id: string) => - api.put(`/api/v1/admin/users/${id}/deactivate`).then(r => r.data), + api.put(`/admin/users/${id}/deactivate`).then(r => r.data), activateUser: (id: string) => - api.put(`/api/v1/admin/users/${id}/activate`).then(r => r.data), + api.put(`/admin/users/${id}/activate`).then(r => r.data), moveUserAccount: (id: string, display_code: string) => - api.put(`/api/v1/admin/users/${id}/move-account`, { display_code }).then(r => r.data), + api.put(`/admin/users/${id}/move-account`, { display_code }).then(r => r.data), // Invite Codes (existing endpoints) listInviteCodes: (params?: Record) => - api.get('/api/v1/invite-codes', { params }).then(r => r.data), + api.get('/invite-codes', { params }).then(r => r.data), createInviteCode: (data?: { expires_at?: string }) => - api.post('/api/v1/invite-codes', data || {}).then(r => r.data), + api.post('/invite-codes', data || {}).then(r => r.data), deleteInviteCode: (id: string) => - api.delete(`/api/v1/invite-codes/${id}`), + api.delete(`/invite-codes/${id}`), // Audit Logs listAuditLogs: (params?: Record) => - api.get('/api/v1/admin/audit-logs', { params }).then(r => r.data), + api.get('/admin/audit-logs', { params }).then(r => r.data), exportAuditLogs: (params?: Record) => - api.get('/api/v1/admin/audit-logs/export', { params, responseType: 'blob' }), + api.get('/admin/audit-logs/export', { params, responseType: 'blob' }), // Plan Limits listPlanLimits: () => - api.get('/api/v1/admin/plan-limits').then(r => r.data), + api.get('/admin/plan-limits').then(r => r.data), updatePlanLimits: (data: PlanLimitConfig) => - api.put('/api/v1/admin/plan-limits', data).then(r => r.data), + api.put('/admin/plan-limits', data).then(r => r.data), // Account Overrides listAccountOverrides: () => - api.get('/api/v1/admin/account-overrides').then(r => r.data), + api.get('/admin/account-overrides').then(r => r.data), createAccountOverride: (data: AccountOverrideCreate) => - api.post('/api/v1/admin/account-overrides', data).then(r => r.data), + api.post('/admin/account-overrides', data).then(r => r.data), updateAccountOverride: (id: string, data: Partial) => - api.put(`/api/v1/admin/account-overrides/${id}`, data).then(r => r.data), + api.put(`/admin/account-overrides/${id}`, data).then(r => r.data), deleteAccountOverride: (id: string) => - api.delete(`/api/v1/admin/account-overrides/${id}`), + api.delete(`/admin/account-overrides/${id}`), // Feature Flags listFeatureFlags: () => - api.get('/api/v1/admin/feature-flags').then(r => r.data), + api.get('/admin/feature-flags').then(r => r.data), createFeatureFlag: (data: FeatureFlagCreate) => - api.post('/api/v1/admin/feature-flags', data).then(r => r.data), + api.post('/admin/feature-flags', data).then(r => r.data), updateFeatureFlag: (id: string, data: Partial) => - api.put(`/api/v1/admin/feature-flags/${id}`, data).then(r => r.data), + api.put(`/admin/feature-flags/${id}`, data).then(r => r.data), deleteFeatureFlag: (id: string) => - api.delete(`/api/v1/admin/feature-flags/${id}`), + api.delete(`/admin/feature-flags/${id}`), updatePlanDefault: (data: PlanDefaultUpdate) => - api.put('/api/v1/admin/feature-flags/plan-defaults', data).then(r => r.data), + api.put('/admin/feature-flags/plan-defaults', data).then(r => r.data), // Feature Flag Account Overrides listFeatureFlagOverrides: () => - api.get('/api/v1/admin/feature-flags/account-overrides').then(r => r.data), + api.get('/admin/feature-flags/account-overrides').then(r => r.data), createFeatureFlagOverride: (data: AccountFeatureOverrideCreate) => - api.post('/api/v1/admin/feature-flags/account-overrides', data).then(r => r.data), + api.post('/admin/feature-flags/account-overrides', data).then(r => r.data), deleteFeatureFlagOverride: (id: string) => - api.delete(`/api/v1/admin/feature-flags/account-overrides/${id}`), + api.delete(`/admin/feature-flags/account-overrides/${id}`), // Platform Settings listSettings: () => - api.get<{ settings: Record }>('/api/v1/admin/settings').then(r => r.data), + api.get<{ settings: Record }>('/admin/settings').then(r => r.data), updateSettings: (settings: Record) => - api.put<{ settings: Record }>('/api/v1/admin/settings', { settings }).then(r => r.data), + api.put<{ settings: Record }>('/admin/settings', { settings }).then(r => r.data), // Global Categories listGlobalCategories: () => - api.get('/api/v1/admin/categories/global').then(r => r.data), + api.get('/admin/categories/global').then(r => r.data), createGlobalCategory: (data: GlobalCategoryCreate) => - api.post('/api/v1/admin/categories/global', data).then(r => r.data), + api.post('/admin/categories/global', data).then(r => r.data), updateGlobalCategory: (id: string, data: Partial) => - api.put(`/api/v1/admin/categories/global/${id}`, data).then(r => r.data), + api.put(`/admin/categories/global/${id}`, data).then(r => r.data), deleteGlobalCategory: (id: string) => - api.delete(`/api/v1/admin/categories/global/${id}`), + api.delete(`/admin/categories/global/${id}`), } export default adminApi diff --git a/frontend/src/components/admin/ActionMenu.tsx b/frontend/src/components/admin/ActionMenu.tsx index cdafab8d..24ce3dee 100644 --- a/frontend/src/components/admin/ActionMenu.tsx +++ b/frontend/src/components/admin/ActionMenu.tsx @@ -1,4 +1,5 @@ import { useState, useRef, useEffect, type ReactNode } from 'react' +import { createPortal } from 'react-dom' import { MoreHorizontal } from 'lucide-react' import { cn } from '@/lib/utils' @@ -16,12 +17,29 @@ interface ActionMenuProps { export function ActionMenu({ items }: ActionMenuProps) { const [open, setOpen] = useState(false) - const ref = useRef(null) + const buttonRef = useRef(null) + const menuRef = useRef(null) + const [menuPosition, setMenuPosition] = useState({ top: 0, right: 0 }) + // Calculate menu position when opened + useEffect(() => { + if (open && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect() + setMenuPosition({ + top: rect.bottom + window.scrollY + 4, // 4px gap (mt-1) + right: window.innerWidth - rect.right + window.scrollX, + }) + } + }, [open]) + + // Close menu when clicking outside useEffect(() => { if (!open) return const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { + if ( + buttonRef.current && !buttonRef.current.contains(e.target as Node) && + menuRef.current && !menuRef.current.contains(e.target as Node) + ) { setOpen(false) } } @@ -30,8 +48,9 @@ export function ActionMenu({ items }: ActionMenuProps) { }, [open]) return ( -
+ <> - {open && ( -
+ {open && createPortal( +
{items.map((item) => (
+
, + document.body )} -
+ ) } diff --git a/frontend/src/pages/account/TeamCategoriesPage.tsx b/frontend/src/pages/account/TeamCategoriesPage.tsx index a142a1cf..02992ba8 100644 --- a/frontend/src/pages/account/TeamCategoriesPage.tsx +++ b/frontend/src/pages/account/TeamCategoriesPage.tsx @@ -23,7 +23,7 @@ export function TeamCategoriesPage() { const fetchData = useCallback(async () => { setLoading(true) try { - const res = await api.get('/api/v1/categories') + const res = await api.get('/categories') setCategories(res.data) } catch { toast.error('Failed to load categories') @@ -39,7 +39,7 @@ export function TeamCategoriesPage() { const handleCreate = async () => { try { - await api.post('/api/v1/categories', form) + await api.post('/categories', form) toast.success('Category created') setCreateOpen(false) setForm({ name: '', slug: '', description: '' }) @@ -52,7 +52,7 @@ export function TeamCategoriesPage() { const handleUpdate = async () => { if (!editCategory) return try { - await api.put(`/api/v1/categories/${editCategory.id}`, form) + await api.put(`/categories/${editCategory.id}`, form) toast.success('Category updated') setEditCategory(null) setForm({ name: '', slug: '', description: '' }) @@ -64,7 +64,7 @@ export function TeamCategoriesPage() { const handleDelete = async (id: string) => { try { - await api.delete(`/api/v1/categories/${id}`) + await api.delete(`/categories/${id}`) toast.success('Category deleted') fetchData() } catch {