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 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-08 06:53:21 -05:00
parent 159161aa59
commit f4eb3fe186
3 changed files with 75 additions and 48 deletions

View File

@@ -18,91 +18,91 @@ import type {
export const adminApi = { export const adminApi = {
// Dashboard // Dashboard
getDashboardMetrics: () => getDashboardMetrics: () =>
api.get<DashboardMetrics>('/api/v1/admin/dashboard/metrics').then(r => r.data), api.get<DashboardMetrics>('/admin/dashboard/metrics').then(r => r.data),
getDashboardActivity: () => getDashboardActivity: () =>
api.get<ActivityEntry[]>('/api/v1/admin/dashboard/activity').then(r => r.data), api.get<ActivityEntry[]>('/admin/dashboard/activity').then(r => r.data),
// Users (existing endpoints) // Users (existing endpoints)
listUsers: (params?: Record<string, unknown>) => listUsers: (params?: Record<string, unknown>) =>
api.get('/api/v1/admin/users', { params }).then(r => r.data), api.get('/admin/users', { params }).then(r => r.data),
getUser: (id: string) => 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) => 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) => 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) => 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) => 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) => 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) // Invite Codes (existing endpoints)
listInviteCodes: (params?: Record<string, unknown>) => listInviteCodes: (params?: Record<string, unknown>) =>
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 }) => 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) => deleteInviteCode: (id: string) =>
api.delete(`/api/v1/invite-codes/${id}`), api.delete(`/invite-codes/${id}`),
// Audit Logs // Audit Logs
listAuditLogs: (params?: Record<string, unknown>) => listAuditLogs: (params?: Record<string, unknown>) =>
api.get<AuditLogListResponse>('/api/v1/admin/audit-logs', { params }).then(r => r.data), api.get<AuditLogListResponse>('/admin/audit-logs', { params }).then(r => r.data),
exportAuditLogs: (params?: Record<string, string>) => exportAuditLogs: (params?: Record<string, string>) =>
api.get('/api/v1/admin/audit-logs/export', { params, responseType: 'blob' }), api.get('/admin/audit-logs/export', { params, responseType: 'blob' }),
// Plan Limits // Plan Limits
listPlanLimits: () => listPlanLimits: () =>
api.get<PlanLimitConfig[]>('/api/v1/admin/plan-limits').then(r => r.data), api.get<PlanLimitConfig[]>('/admin/plan-limits').then(r => r.data),
updatePlanLimits: (data: PlanLimitConfig) => updatePlanLimits: (data: PlanLimitConfig) =>
api.put<PlanLimitConfig>('/api/v1/admin/plan-limits', data).then(r => r.data), api.put<PlanLimitConfig>('/admin/plan-limits', data).then(r => r.data),
// Account Overrides // Account Overrides
listAccountOverrides: () => listAccountOverrides: () =>
api.get<AccountOverrideResponse[]>('/api/v1/admin/account-overrides').then(r => r.data), api.get<AccountOverrideResponse[]>('/admin/account-overrides').then(r => r.data),
createAccountOverride: (data: AccountOverrideCreate) => createAccountOverride: (data: AccountOverrideCreate) =>
api.post<AccountOverrideResponse>('/api/v1/admin/account-overrides', data).then(r => r.data), api.post<AccountOverrideResponse>('/admin/account-overrides', data).then(r => r.data),
updateAccountOverride: (id: string, data: Partial<AccountOverrideCreate>) => updateAccountOverride: (id: string, data: Partial<AccountOverrideCreate>) =>
api.put<AccountOverrideResponse>(`/api/v1/admin/account-overrides/${id}`, data).then(r => r.data), api.put<AccountOverrideResponse>(`/admin/account-overrides/${id}`, data).then(r => r.data),
deleteAccountOverride: (id: string) => deleteAccountOverride: (id: string) =>
api.delete(`/api/v1/admin/account-overrides/${id}`), api.delete(`/admin/account-overrides/${id}`),
// Feature Flags // Feature Flags
listFeatureFlags: () => listFeatureFlags: () =>
api.get<FeatureFlagResponse[]>('/api/v1/admin/feature-flags').then(r => r.data), api.get<FeatureFlagResponse[]>('/admin/feature-flags').then(r => r.data),
createFeatureFlag: (data: FeatureFlagCreate) => createFeatureFlag: (data: FeatureFlagCreate) =>
api.post<FeatureFlagResponse>('/api/v1/admin/feature-flags', data).then(r => r.data), api.post<FeatureFlagResponse>('/admin/feature-flags', data).then(r => r.data),
updateFeatureFlag: (id: string, data: Partial<FeatureFlagCreate>) => updateFeatureFlag: (id: string, data: Partial<FeatureFlagCreate>) =>
api.put<FeatureFlagResponse>(`/api/v1/admin/feature-flags/${id}`, data).then(r => r.data), api.put<FeatureFlagResponse>(`/admin/feature-flags/${id}`, data).then(r => r.data),
deleteFeatureFlag: (id: string) => deleteFeatureFlag: (id: string) =>
api.delete(`/api/v1/admin/feature-flags/${id}`), api.delete(`/admin/feature-flags/${id}`),
updatePlanDefault: (data: PlanDefaultUpdate) => 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 // Feature Flag Account Overrides
listFeatureFlagOverrides: () => listFeatureFlagOverrides: () =>
api.get<AccountFeatureOverrideResponse[]>('/api/v1/admin/feature-flags/account-overrides').then(r => r.data), api.get<AccountFeatureOverrideResponse[]>('/admin/feature-flags/account-overrides').then(r => r.data),
createFeatureFlagOverride: (data: AccountFeatureOverrideCreate) => createFeatureFlagOverride: (data: AccountFeatureOverrideCreate) =>
api.post<AccountFeatureOverrideResponse>('/api/v1/admin/feature-flags/account-overrides', data).then(r => r.data), api.post<AccountFeatureOverrideResponse>('/admin/feature-flags/account-overrides', data).then(r => r.data),
deleteFeatureFlagOverride: (id: string) => deleteFeatureFlagOverride: (id: string) =>
api.delete(`/api/v1/admin/feature-flags/account-overrides/${id}`), api.delete(`/admin/feature-flags/account-overrides/${id}`),
// Platform Settings // Platform Settings
listSettings: () => listSettings: () =>
api.get<{ settings: Record<string, unknown> }>('/api/v1/admin/settings').then(r => r.data), api.get<{ settings: Record<string, unknown> }>('/admin/settings').then(r => r.data),
updateSettings: (settings: Record<string, unknown>) => updateSettings: (settings: Record<string, unknown>) =>
api.put<{ settings: Record<string, unknown> }>('/api/v1/admin/settings', { settings }).then(r => r.data), api.put<{ settings: Record<string, unknown> }>('/admin/settings', { settings }).then(r => r.data),
// Global Categories // Global Categories
listGlobalCategories: () => listGlobalCategories: () =>
api.get<AdminCategory[]>('/api/v1/admin/categories/global').then(r => r.data), api.get<AdminCategory[]>('/admin/categories/global').then(r => r.data),
createGlobalCategory: (data: GlobalCategoryCreate) => createGlobalCategory: (data: GlobalCategoryCreate) =>
api.post<AdminCategory>('/api/v1/admin/categories/global', data).then(r => r.data), api.post<AdminCategory>('/admin/categories/global', data).then(r => r.data),
updateGlobalCategory: (id: string, data: Partial<GlobalCategoryCreate>) => updateGlobalCategory: (id: string, data: Partial<GlobalCategoryCreate>) =>
api.put<AdminCategory>(`/api/v1/admin/categories/global/${id}`, data).then(r => r.data), api.put<AdminCategory>(`/admin/categories/global/${id}`, data).then(r => r.data),
deleteGlobalCategory: (id: string) => deleteGlobalCategory: (id: string) =>
api.delete(`/api/v1/admin/categories/global/${id}`), api.delete(`/admin/categories/global/${id}`),
} }
export default adminApi export default adminApi

View File

@@ -1,4 +1,5 @@
import { useState, useRef, useEffect, type ReactNode } from 'react' import { useState, useRef, useEffect, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { MoreHorizontal } from 'lucide-react' import { MoreHorizontal } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -16,12 +17,29 @@ interface ActionMenuProps {
export function ActionMenu({ items }: ActionMenuProps) { export function ActionMenu({ items }: ActionMenuProps) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(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(() => { useEffect(() => {
if (!open) return if (!open) return
const handler = (e: MouseEvent) => { 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) setOpen(false)
} }
} }
@@ -30,8 +48,9 @@ export function ActionMenu({ items }: ActionMenuProps) {
}, [open]) }, [open])
return ( return (
<div className="relative" ref={ref}> <>
<button <button
ref={buttonRef}
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
className={cn( className={cn(
'rounded-md p-1.5 text-muted-foreground transition-colors', 'rounded-md p-1.5 text-muted-foreground transition-colors',
@@ -40,11 +59,18 @@ export function ActionMenu({ items }: ActionMenuProps) {
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</button> </button>
{open && ( {open && createPortal(
<div className={cn( <div
'absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-md border border-border', ref={menuRef}
'bg-card py-1 shadow-lg animate-scale-in' className={cn(
)}> 'fixed z-50 min-w-[160px] rounded-md border border-border',
'bg-card py-1 shadow-lg animate-scale-in'
)}
style={{
top: `${menuPosition.top}px`,
right: `${menuPosition.right}px`,
}}
>
{items.map((item) => ( {items.map((item) => (
<button <button
key={item.label} key={item.label}
@@ -62,9 +88,10 @@ export function ActionMenu({ items }: ActionMenuProps) {
{item.label} {item.label}
</button> </button>
))} ))}
</div> </div>,
document.body
)} )}
</div> </>
) )
} }

View File

@@ -23,7 +23,7 @@ export function TeamCategoriesPage() {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
const res = await api.get('/api/v1/categories') const res = await api.get('/categories')
setCategories(res.data) setCategories(res.data)
} catch { } catch {
toast.error('Failed to load categories') toast.error('Failed to load categories')
@@ -39,7 +39,7 @@ export function TeamCategoriesPage() {
const handleCreate = async () => { const handleCreate = async () => {
try { try {
await api.post('/api/v1/categories', form) await api.post('/categories', form)
toast.success('Category created') toast.success('Category created')
setCreateOpen(false) setCreateOpen(false)
setForm({ name: '', slug: '', description: '' }) setForm({ name: '', slug: '', description: '' })
@@ -52,7 +52,7 @@ export function TeamCategoriesPage() {
const handleUpdate = async () => { const handleUpdate = async () => {
if (!editCategory) return if (!editCategory) return
try { try {
await api.put(`/api/v1/categories/${editCategory.id}`, form) await api.put(`/categories/${editCategory.id}`, form)
toast.success('Category updated') toast.success('Category updated')
setEditCategory(null) setEditCategory(null)
setForm({ name: '', slug: '', description: '' }) setForm({ name: '', slug: '', description: '' })
@@ -64,7 +64,7 @@ export function TeamCategoriesPage() {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
try { try {
await api.delete(`/api/v1/categories/${id}`) await api.delete(`/categories/${id}`)
toast.success('Category deleted') toast.success('Category deleted')
fetchData() fetchData()
} catch { } catch {