Inline filtering results, select-all across pages, country flags, settings RBAC, and inline role changes
- Round detail: add skeleton loading for filtering stats, inline results table with expandable rows, pagination, override/reinstate, CSV export, and tooltip on AI summaries button (removes need for separate results page) - Projects: add select-all-across-pages with Gmail-style banner, show country flags with tooltip instead of country codes (table + card views), add listAllIds backend endpoint - Settings: allow PROGRAM_ADMIN access to settings page, restrict infrastructure tabs (AI, Email, Storage, Security, Webhooks) to SUPER_ADMIN only - Members: add inline role change via dropdown submenu in user actions, enforce role hierarchy (only super admins can modify admin/super-admin roles) in both backend and UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -42,11 +42,16 @@ const TAB_ROLES: Record<TabKey, RoleValue[] | undefined> = {
|
||||
}
|
||||
|
||||
const statusColors: Record<string, 'default' | 'success' | 'secondary' | 'destructive'> = {
|
||||
NONE: 'secondary',
|
||||
ACTIVE: 'success',
|
||||
INVITED: 'secondary',
|
||||
SUSPENDED: 'destructive',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
NONE: 'Not Invited',
|
||||
}
|
||||
|
||||
const roleColors: Record<string, 'default' | 'outline' | 'secondary'> = {
|
||||
JURY_MEMBER: 'default',
|
||||
MENTOR: 'secondary',
|
||||
@@ -92,6 +97,9 @@ export function MembersContent() {
|
||||
|
||||
const roles = TAB_ROLES[tab]
|
||||
|
||||
const { data: currentUser } = trpc.user.me.useQuery()
|
||||
const currentUserRole = currentUser?.role as RoleValue | undefined
|
||||
|
||||
const { data, isLoading } = trpc.user.list.useQuery({
|
||||
roles: roles,
|
||||
search: search || undefined,
|
||||
@@ -216,7 +224,7 @@ export function MembersContent() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{user.status}
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -233,6 +241,8 @@ export function MembersContent() {
|
||||
userId={user.id}
|
||||
userEmail={user.email}
|
||||
userStatus={user.status}
|
||||
userRole={user.role as RoleValue}
|
||||
currentUserRole={currentUserRole}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -263,7 +273,7 @@ export function MembersContent() {
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{user.status}
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -305,6 +315,8 @@ export function MembersContent() {
|
||||
userId={user.id}
|
||||
userEmail={user.email}
|
||||
userStatus={user.status}
|
||||
userRole={user.role as RoleValue}
|
||||
currentUserRole={currentUserRole}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
@@ -28,15 +31,29 @@ import {
|
||||
UserCog,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Shield,
|
||||
Check,
|
||||
} from 'lucide-react'
|
||||
|
||||
type Role = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
|
||||
|
||||
const ROLE_LABELS: Record<Role, string> = {
|
||||
SUPER_ADMIN: 'Super Admin',
|
||||
PROGRAM_ADMIN: 'Program Admin',
|
||||
JURY_MEMBER: 'Jury Member',
|
||||
MENTOR: 'Mentor',
|
||||
OBSERVER: 'Observer',
|
||||
}
|
||||
|
||||
interface UserActionsProps {
|
||||
userId: string
|
||||
userEmail: string
|
||||
userStatus: string
|
||||
userRole: Role
|
||||
currentUserRole?: Role
|
||||
}
|
||||
|
||||
export function UserActions({ userId, userEmail, userStatus }: UserActionsProps) {
|
||||
export function UserActions({ userId, userEmail, userStatus, userRole, currentUserRole }: UserActionsProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
|
||||
@@ -44,13 +61,40 @@ export function UserActions({ userId, userEmail, userStatus }: UserActionsProps)
|
||||
const sendInvitation = trpc.user.sendInvitation.useMutation()
|
||||
const deleteUser = trpc.user.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
// Invalidate user list to refresh the members table
|
||||
utils.user.list.invalidate()
|
||||
},
|
||||
})
|
||||
const updateUser = trpc.user.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.user.list.invalidate()
|
||||
toast.success('Role updated successfully')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || 'Failed to update role')
|
||||
},
|
||||
})
|
||||
|
||||
const isSuperAdmin = currentUserRole === 'SUPER_ADMIN'
|
||||
|
||||
// Determine which roles can be assigned
|
||||
const getAvailableRoles = (): Role[] => {
|
||||
if (isSuperAdmin) {
|
||||
return ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']
|
||||
}
|
||||
// Program admins can only assign lower roles
|
||||
return ['JURY_MEMBER', 'MENTOR', 'OBSERVER']
|
||||
}
|
||||
|
||||
// Can this user's role be changed by the current user?
|
||||
const canChangeRole = isSuperAdmin || (!['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(userRole))
|
||||
|
||||
const handleRoleChange = (newRole: Role) => {
|
||||
if (newRole === userRole) return
|
||||
updateUser.mutate({ id: userId, role: newRole })
|
||||
}
|
||||
|
||||
const handleSendInvitation = async () => {
|
||||
if (userStatus !== 'INVITED') {
|
||||
if (userStatus !== 'NONE' && userStatus !== 'INVITED') {
|
||||
toast.error('User has already accepted their invitation')
|
||||
return
|
||||
}
|
||||
@@ -98,9 +142,31 @@ export function UserActions({ userId, userEmail, userStatus }: UserActionsProps)
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{canChangeRole && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger disabled={updateUser.isPending}>
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
{updateUser.isPending ? 'Updating...' : 'Change Role'}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{getAvailableRoles().map((role) => (
|
||||
<DropdownMenuItem
|
||||
key={role}
|
||||
onClick={() => handleRoleChange(role)}
|
||||
disabled={role === userRole}
|
||||
>
|
||||
{role === userRole && <Check className="mr-2 h-4 w-4" />}
|
||||
<span className={role === userRole ? 'font-medium' : role !== userRole ? 'ml-6' : ''}>
|
||||
{ROLE_LABELS[role]}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={handleSendInvitation}
|
||||
disabled={userStatus !== 'INVITED' || isSending}
|
||||
disabled={(userStatus !== 'NONE' && userStatus !== 'INVITED') || isSending}
|
||||
>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
{isSending ? 'Sending...' : 'Send Invite'}
|
||||
@@ -147,18 +213,35 @@ interface UserMobileActionsProps {
|
||||
userId: string
|
||||
userEmail: string
|
||||
userStatus: string
|
||||
userRole: Role
|
||||
currentUserRole?: Role
|
||||
}
|
||||
|
||||
export function UserMobileActions({
|
||||
userId,
|
||||
userEmail,
|
||||
userStatus,
|
||||
userRole,
|
||||
currentUserRole,
|
||||
}: UserMobileActionsProps) {
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
const utils = trpc.useUtils()
|
||||
const sendInvitation = trpc.user.sendInvitation.useMutation()
|
||||
const updateUser = trpc.user.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.user.list.invalidate()
|
||||
toast.success('Role updated successfully')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || 'Failed to update role')
|
||||
},
|
||||
})
|
||||
|
||||
const isSuperAdmin = currentUserRole === 'SUPER_ADMIN'
|
||||
const canChangeRole = isSuperAdmin || (!['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(userRole))
|
||||
|
||||
const handleSendInvitation = async () => {
|
||||
if (userStatus !== 'INVITED') {
|
||||
if (userStatus !== 'NONE' && userStatus !== 'INVITED') {
|
||||
toast.error('User has already accepted their invitation')
|
||||
return
|
||||
}
|
||||
@@ -175,27 +258,46 @@ export function UserMobileActions({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex-1" asChild>
|
||||
<Link href={`/admin/members/${userId}`}>
|
||||
<UserCog className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={handleSendInvitation}
|
||||
disabled={userStatus !== 'INVITED' || isSending}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Invite
|
||||
</Button>
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1" asChild>
|
||||
<Link href={`/admin/members/${userId}`}>
|
||||
<UserCog className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={handleSendInvitation}
|
||||
disabled={(userStatus !== 'NONE' && userStatus !== 'INVITED') || isSending}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
{canChangeRole && (
|
||||
<select
|
||||
value={userRole}
|
||||
onChange={(e) => updateUser.mutate({ id: userId, role: e.target.value as Role })}
|
||||
disabled={updateUser.isPending}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
|
||||
>
|
||||
{(isSuperAdmin
|
||||
? (['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'] as Role[])
|
||||
: (['JURY_MEMBER', 'MENTOR', 'OBSERVER'] as Role[])
|
||||
).map((role) => (
|
||||
<option key={role} value={role}>
|
||||
{ROLE_LABELS[role]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,9 +61,10 @@ function SettingsSkeleton() {
|
||||
|
||||
interface SettingsContentProps {
|
||||
initialSettings: Record<string, string>
|
||||
isSuperAdmin?: boolean
|
||||
}
|
||||
|
||||
export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
export function SettingsContent({ initialSettings, isSuperAdmin = true }: SettingsContentProps) {
|
||||
// We use the initial settings passed from the server
|
||||
// Forms will refetch on mutation success
|
||||
|
||||
@@ -168,10 +169,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<Globe className="h-4 w-4" />
|
||||
Locale
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="email" className="gap-2 shrink-0">
|
||||
<Mail className="h-4 w-4" />
|
||||
Email
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="email" className="gap-2 shrink-0">
|
||||
<Mail className="h-4 w-4" />
|
||||
Email
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="notifications" className="gap-2 shrink-0">
|
||||
<Bell className="h-4 w-4" />
|
||||
Notif.
|
||||
@@ -180,18 +183,22 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<Newspaper className="h-4 w-4" />
|
||||
Digest
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="gap-2 shrink-0">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="security" className="gap-2 shrink-0">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="audit" className="gap-2 shrink-0">
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
Audit
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ai" className="gap-2 shrink-0">
|
||||
<Bot className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="ai" className="gap-2 shrink-0">
|
||||
<Bot className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="tags" className="gap-2 shrink-0">
|
||||
<Tags className="h-4 w-4" />
|
||||
Tags
|
||||
@@ -200,10 +207,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
Analytics
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="storage" className="gap-2 shrink-0">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="storage" className="gap-2 shrink-0">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<div className="lg:flex lg:gap-8">
|
||||
@@ -230,10 +239,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<div>
|
||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Communication</p>
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Mail className="h-4 w-4" />
|
||||
Email
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Mail className="h-4 w-4" />
|
||||
Email
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="notifications" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Bell className="h-4 w-4" />
|
||||
Notifications
|
||||
@@ -247,10 +258,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<div>
|
||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Security</p>
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="audit" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
Audit
|
||||
@@ -260,10 +273,12 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
<div>
|
||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Features</p>
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Bot className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Bot className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="tags" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Tags className="h-4 w-4" />
|
||||
Tags
|
||||
@@ -274,35 +289,39 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
{isSuperAdmin && (
|
||||
<div>
|
||||
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Storage
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
<TabsContent value="ai" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AI Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure AI-powered features like smart jury assignment
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AISettingsForm settings={aiSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AIUsageCard />
|
||||
</TabsContent>
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="ai" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AI Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure AI-powered features like smart jury assignment
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AISettingsForm settings={aiSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AIUsageCard />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="tags">
|
||||
<Card>
|
||||
@@ -350,19 +369,21 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="email">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure email settings for notifications and magic links
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmailSettingsForm settings={emailSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="email">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure email settings for notifications and magic links
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmailSettingsForm settings={emailSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<Card>
|
||||
@@ -378,33 +399,37 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="storage">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Storage</CardTitle>
|
||||
<CardDescription>
|
||||
Configure file upload limits and allowed types
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StorageSettingsForm settings={storageSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="storage">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Storage</CardTitle>
|
||||
<CardDescription>
|
||||
Configure file upload limits and allowed types
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<StorageSettingsForm settings={storageSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="security">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure security and access control settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SecuritySettingsForm settings={securitySettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="security">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure security and access control settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SecuritySettingsForm settings={securitySettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="defaults">
|
||||
<Card>
|
||||
@@ -502,26 +527,28 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Webhook className="h-4 w-4" />
|
||||
Webhooks
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure webhook endpoints for platform events
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button asChild>
|
||||
<Link href="/admin/settings/webhooks">
|
||||
<Webhook className="mr-2 h-4 w-4" />
|
||||
Manage Webhooks
|
||||
<ExternalLink className="ml-2 h-3 w-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{isSuperAdmin && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Webhook className="h-4 w-4" />
|
||||
Webhooks
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure webhook endpoints for platform events
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button asChild>
|
||||
<Link href="/admin/settings/webhooks">
|
||||
<Webhook className="mr-2 h-4 w-4" />
|
||||
Manage Webhooks
|
||||
<ExternalLink className="ml-2 h-3 w-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user