mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
update ui layout
This commit is contained in:
@@ -59,7 +59,11 @@ export function RunningStateCellAction({ row }: Props) {
|
||||
})
|
||||
}
|
||||
}}>
|
||||
<span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">{t('accounts.viewDetails')}</span>
|
||||
<span
|
||||
className="text-xs text-primary cursor-pointer underline underline-offset-2 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{t('accounts.viewDetails')}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
|
||||
import { Mail, Users, Inbox, Zap } from 'lucide-react';
|
||||
import { Mail, Users, Inbox, Zap, Paperclip } from 'lucide-react';
|
||||
import { formatBytes, formatNumber } from '@/lib/utils';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api';
|
||||
@@ -30,24 +30,6 @@ interface DailyActivity {
|
||||
timestamp_ms: number;
|
||||
}
|
||||
|
||||
const GithubIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
|
||||
<path d="M9 18c-4.51 2-5-2-7-2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
function convertRecentActivity(timeBuckets: TimeBucket[], locale: string): DailyActivity[] {
|
||||
const dateFormatter = new Intl.DateTimeFormat(locale, {
|
||||
month: 'short',
|
||||
@@ -223,7 +205,7 @@ export default function MailArchiveDashboard() {
|
||||
<Card className="md:col-span-2 lg:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.totalAttachments')}</CardTitle>
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
<Paperclip className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xl font-bold">{formatNumber(stats1.attachment_count)}</div>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useMinimalAccountList from '@/hooks/use-minimal-account-list'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { PermissionsDialog } from './permissions-dialog'
|
||||
import Logo from '@/assets/logo.svg'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
|
||||
export function AccountAccessList() {
|
||||
const { t } = useTranslation()
|
||||
const { data: user } = useCurrentUser()
|
||||
const { getEmailById } = useMinimalAccountList()
|
||||
|
||||
const [permissionsOpen, setPermissionsOpen] = useState(false)
|
||||
const [permissionsAccountId, setPermissionsAccountId] = useState<number | undefined>(undefined)
|
||||
|
||||
if (!user) return null
|
||||
|
||||
const accessibleAccountIds = user.account_access_map instanceof Map
|
||||
? Array.from(user.account_access_map.keys())
|
||||
: Object.keys(user.account_access_map || {}).map(Number)
|
||||
|
||||
const roleSummary = user.account_roles_summary || {}
|
||||
|
||||
if (accessibleAccountIds.length === 0) {
|
||||
return (
|
||||
<div className="flex h-[450px] items-center justify-center rounded-md border border-dashed mt-4">
|
||||
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center px-4">
|
||||
<img
|
||||
src={Logo}
|
||||
className="max-h-[100px] w-auto opacity-20 saturate-0 object-contain"
|
||||
alt="Bichon Logo"
|
||||
/>
|
||||
<h3 className="mt-4 text-lg font-semibold">{t('settings.access.empty.title')}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{t('settings.access.empty.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{accessibleAccountIds.map((accountId) => {
|
||||
const email = getEmailById(accountId)
|
||||
const roleName = roleSummary[accountId]
|
||||
if (!email) return null
|
||||
|
||||
return (
|
||||
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit">
|
||||
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold shrink-0">
|
||||
{email.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<CardTitle className="text-sm font-semibold truncate">{email}</CardTitle>
|
||||
<CardDescription className="text-[10px] font-mono">
|
||||
{t('settings.profile.account.id', { id: accountId })}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-between pt-3 border-t">
|
||||
{roleName ? (
|
||||
<Badge variant="secondary" className="text-[11px]">
|
||||
{roleName}
|
||||
</Badge>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs h-7"
|
||||
onClick={() => {
|
||||
setPermissionsAccountId(accountId)
|
||||
setPermissionsOpen(true)
|
||||
}}
|
||||
>
|
||||
{t('settings.profile.button.permissions')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<PermissionsDialog
|
||||
currentRow={user}
|
||||
open={permissionsOpen}
|
||||
onOpenChange={setPermissionsOpen}
|
||||
mode="account"
|
||||
accountId={permissionsAccountId}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+19
-22
@@ -25,11 +25,12 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { getPermissions, User } from '@/api/users/api'
|
||||
import { CheckCircle2, XCircle } from 'lucide-react'
|
||||
import { CheckCircle, XCircle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useMinimalAccountList from '@/hooks/use-minimal-account-list'
|
||||
|
||||
interface Props {
|
||||
currentRow?: User
|
||||
@@ -95,6 +96,7 @@ export function PermissionsDialog({
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getEmailById } = useMinimalAccountList();
|
||||
|
||||
const ownedPermissions = React.useMemo<string[]>(() => {
|
||||
if (!currentRow) return []
|
||||
@@ -120,7 +122,7 @@ export function PermissionsDialog({
|
||||
mode === 'global'
|
||||
? getGlobalCategories(t)
|
||||
: getAccountCategories(t)
|
||||
|
||||
|
||||
const title =
|
||||
mode === 'global'
|
||||
? t('permission.dialog.global_title')
|
||||
@@ -140,56 +142,51 @@ export function PermissionsDialog({
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{mode === 'global'
|
||||
? t('permission.scope.global')
|
||||
: t('permission.scope.account', { id: accountId })}
|
||||
: t('permission.scope.account', { id: getEmailById(accountId!) })}
|
||||
</Badge>
|
||||
</div>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<div className="grid gap-6 px-1">
|
||||
<div className="flex flex-col gap-6 px-1">
|
||||
{categories.map((cat) => (
|
||||
<div key={cat.title} className="flex flex-col">
|
||||
<h3 className="text-[11px] font-bold text-slate-500 border-l-4 border-blue-500 pl-2 mb-4 uppercase tracking-widest">
|
||||
<h3 className="text-[11px] font-bold text-muted-foreground border-l-2 border-primary pl-2 mb-3 uppercase tracking-widest">
|
||||
{cat.title}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
|
||||
{cat.keys.map((key) => {
|
||||
const item = permissions.get(key)
|
||||
if (!item) return null
|
||||
|
||||
const hasPermission =
|
||||
ownedPermissions.includes(item.value)
|
||||
const hasPermission = ownedPermissions.includes(item.value)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 p-2 rounded-md transition-all text-xs border',
|
||||
'flex items-center gap-2.5 p-2.5 rounded-md border text-xs transition-all',
|
||||
hasPermission
|
||||
? 'bg-green-50/40 border-green-100 text-green-800 shadow-sm'
|
||||
: 'bg-slate-50/30 border-transparent text-slate-400 opacity-60',
|
||||
? 'bg-primary/5 border-primary/20'
|
||||
: 'bg-muted/30 border-border opacity-50',
|
||||
)}
|
||||
>
|
||||
{hasPermission ? (
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-green-600 shrink-0" />
|
||||
<CheckCircle className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<XCircle className="w-3.5 h-3.5 text-slate-300 shrink-0" />
|
||||
<XCircle className="w-4 h-4 text-muted-foreground/40 shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
'font-semibold text-sm leading-none truncate',
|
||||
hasPermission
|
||||
? 'text-slate-900'
|
||||
: 'text-slate-500',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'font-medium text-xs leading-none truncate',
|
||||
hasPermission ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-xs opacity-70 font-mono mt-1 truncate">
|
||||
<span className="text-[10px] text-muted-foreground font-mono mt-1 truncate">
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@
|
||||
import { Outlet } from '@tanstack/react-router'
|
||||
import { Main } from '@/components/layout/main'
|
||||
import SidebarNav from './components/sidebar-nav'
|
||||
import { KeyRound, Palette, SettingsIcon, UserCog, Waypoints } from 'lucide-react'
|
||||
import { KeyRound, Palette, SettingsIcon, ShieldCheck, UserCog, Waypoints } from 'lucide-react'
|
||||
import { FixedHeader } from '@/components/layout/fixed-header'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -36,6 +36,11 @@ export default function Settings() {
|
||||
href: '/settings/profile',
|
||||
icon: <UserCog size={18} />,
|
||||
},
|
||||
{
|
||||
title: t('settings.sidebar.access'),
|
||||
href: '/settings/access',
|
||||
icon: <ShieldCheck size={18} />
|
||||
},
|
||||
{
|
||||
title: t('settings.appearance.title'),
|
||||
href: '/settings/appearance',
|
||||
|
||||
@@ -35,17 +35,13 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { PasswordInput } from '@/components/password-input'
|
||||
import { update_user, User } from '@/api/users/api'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { FileWithPreview } from '@/hooks/use-file-upload'
|
||||
import AvatarUpload from './avatar-upload'
|
||||
import useMinimalAccountList from '@/hooks/use-minimal-account-list'
|
||||
import { PermissionsDialog } from './permissions-dialog'
|
||||
import { PermissionsDialog } from '../access/permissions-dialog'
|
||||
|
||||
const profileSchema = (t: (key: string) => string) => z.object({
|
||||
username: z
|
||||
@@ -104,10 +100,7 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
|
||||
const [avatarFile, setAvatarFile] = useState<FileWithPreview | null>(null)
|
||||
|
||||
const [permissionsOpen, setPermissionsOpen] = useState(false)
|
||||
const [permissionsMode, setPermissionsMode] =
|
||||
useState<'global' | 'account'>('global')
|
||||
const [permissionsAccountId, setPermissionsAccountId] =
|
||||
useState<number | undefined>(undefined)
|
||||
const [permissionsAccountId, setPermissionsAccountId] = useState<number | undefined>(undefined)
|
||||
|
||||
const form = useForm<ProfileFormValues>({
|
||||
resolver: zodResolver(profileSchema(t)),
|
||||
@@ -149,15 +142,7 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
|
||||
},
|
||||
})
|
||||
|
||||
const { getEmailById } = useMinimalAccountList()
|
||||
|
||||
const accessibleAccountIds = user.account_access_map instanceof Map
|
||||
? Array.from(user.account_access_map.keys())
|
||||
: Object.keys(user.account_access_map || {}).map(Number)
|
||||
|
||||
const hasAccess = accessibleAccountIds.length > 0
|
||||
const roleNames = user.global_roles_names
|
||||
const roleSummary = user.account_roles_summary || {}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -188,7 +173,6 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => {
|
||||
setPermissionsMode('global')
|
||||
setPermissionsAccountId(undefined)
|
||||
setPermissionsOpen(true)
|
||||
}}
|
||||
@@ -260,77 +244,6 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasAccess && (
|
||||
<Separator orientation="vertical" className="hidden lg:block" />
|
||||
)}
|
||||
|
||||
{hasAccess && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{t('settings.profile.section.accounts', {
|
||||
count: accessibleAccountIds.length,
|
||||
})}
|
||||
</h2>
|
||||
|
||||
<ScrollArea className="h-[calc(100vh-16rem)] pr-4">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{accessibleAccountIds.map((accountId) => {
|
||||
const email = getEmailById(accountId)
|
||||
const roleName = roleSummary[accountId]
|
||||
if (!email) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={accountId}
|
||||
className="group flex items-center justify-between p-3 rounded-xl border bg-card hover:bg-accent/40 transition-all shadow-sm"
|
||||
>
|
||||
<div className="flex items-center min-w-0">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold mr-4 shrink-0">
|
||||
{email.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs font-semibold truncate">
|
||||
{email}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
{t('settings.profile.account.id', {
|
||||
id: accountId,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{roleName && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[11px]"
|
||||
>
|
||||
{roleName}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="text-[10px]"
|
||||
onClick={() => {
|
||||
setPermissionsMode('account')
|
||||
setPermissionsAccountId(accountId)
|
||||
setPermissionsOpen(true)
|
||||
}}
|
||||
>
|
||||
{t('settings.profile.button.permissions')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start pt-4">
|
||||
@@ -346,12 +259,11 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<PermissionsDialog
|
||||
currentRow={user}
|
||||
open={permissionsOpen}
|
||||
onOpenChange={setPermissionsOpen}
|
||||
mode={permissionsMode}
|
||||
mode="global"
|
||||
accountId={permissionsAccountId}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -41,7 +41,11 @@ export function PermissionsCellAction({ row }: Props) {
|
||||
setCurrentRow(row.original)
|
||||
setOpen('permissions')
|
||||
}}>
|
||||
<span className="text-xs text-blue-500 cursor-pointer underline hover:text-blue-700">{t('roles.details.view_permissions')}</span>
|
||||
<span
|
||||
className="text-xs text-primary cursor-pointer underline underline-offset-2 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
{t('roles.details.view_permissions')}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { getPermissions, UserRole } from '@/api/users/api'
|
||||
import { CheckCircle2, XCircle } from 'lucide-react'
|
||||
import { CheckCircle, XCircle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -71,7 +71,7 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-7xl w-[90vw] overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<DialogContent className="max-w-7xl w-[40vw] overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<DialogHeader className="pb-4 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<DialogTitle>
|
||||
@@ -87,19 +87,15 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
: t('roles.details.desc_account')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<div className={cn(
|
||||
"grid gap-6 px-1",
|
||||
roleType === 'Global' ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1"
|
||||
)}>
|
||||
<div className="flex flex-col gap-6 px-1">
|
||||
{categories.map((cat) => (
|
||||
<div key={cat.titleKey} className="flex flex-col">
|
||||
<h3 className="text-[11px] font-bold text-slate-500 border-l-4 border-blue-500 pl-2 mb-4 uppercase tracking-widest">
|
||||
<h3 className="text-[11px] font-bold text-muted-foreground border-l-2 border-primary pl-2 mb-3 uppercase tracking-widest">
|
||||
{t(cat.titleKey)}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
|
||||
{cat.keys.map((key) => {
|
||||
const item = getPermissions(t).find(p => p.value === key);
|
||||
if (!item) return null;
|
||||
@@ -110,26 +106,23 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 p-2 rounded-md transition-all text-xs border",
|
||||
"flex items-center gap-2.5 p-2.5 rounded-md border text-xs transition-all",
|
||||
hasPermission
|
||||
? "bg-green-50/40 border-green-100 text-green-800 shadow-sm"
|
||||
: "bg-slate-50/30 border-transparent text-slate-400 opacity-60"
|
||||
? "bg-primary/5 border-primary/20 text-foreground"
|
||||
: "bg-muted/30 border-border text-muted-foreground opacity-50"
|
||||
)}
|
||||
>
|
||||
{hasPermission ? (
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-green-600 shrink-0" />
|
||||
<CheckCircle className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<XCircle className="w-3.5 h-3.5 text-slate-300 shrink-0" />
|
||||
<XCircle className="w-4 h-4 text-muted-foreground/30 shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className={cn(
|
||||
"font-semibold text-sm leading-none truncate",
|
||||
hasPermission ? "text-slate-900" : "text-slate-500"
|
||||
)}>
|
||||
<span className="font-medium text-xs leading-none truncate">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-xs opacity-70 font-mono mt-1 truncate">
|
||||
<span className="text-[10px] text-muted-foreground font-mono mt-1 truncate">
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -141,7 +134,6 @@ export function PermissionsDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -223,7 +223,7 @@ export function UserActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
<DialogHeader className="p-6 pb-0 shrink-0">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
{isEdit && currentRow?.avatar ? (
|
||||
<img src={`data:image/png;base64,${currentRow.avatar}`} className="h-12 w-12 rounded-full border shadow-sm" alt="" />
|
||||
<img src={`data:image/png;base64,${currentRow.avatar}`} className="h-12 w-12 rounded-full border shadow-sm object-cover" alt="" />
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<UserIcon className="h-6 w-6" />
|
||||
|
||||
Reference in New Issue
Block a user