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:
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// 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 React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { cn } from '@/lib/utils';
|
||||
import 'ace-builds/src-noconflict/mode-handlebars';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/mode-markdown';
|
||||
import 'ace-builds/src-noconflict/theme-kuroir';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
|
||||
interface ReactAceEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
readOnly?: boolean;
|
||||
theme?: 'kuroir' | 'monokai';
|
||||
mode?: 'handlebars' | 'json' | 'markdown' | 'python';
|
||||
}
|
||||
|
||||
const ReactAceEditor: React.FC<ReactAceEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
readOnly = false,
|
||||
theme = 'github',
|
||||
mode = 'handlebars'
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('w-full h-[100px]', className)}>
|
||||
<AceEditor
|
||||
mode={mode}
|
||||
theme={theme}
|
||||
readOnly={readOnly}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
fontSize={14}
|
||||
showPrintMargin={false}
|
||||
showGutter={true}
|
||||
highlightActiveLine={true}
|
||||
width="100%"
|
||||
height="100%"
|
||||
setOptions={{
|
||||
useWorker: false,
|
||||
enableBasicAutocompletion: true,
|
||||
enableMobileMenu: true,
|
||||
enableLiveAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
showLineNumbers: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactAceEditor;
|
||||
@@ -1,113 +0,0 @@
|
||||
//
|
||||
// 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 React from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
IconArrowRightDashed,
|
||||
IconMoon,
|
||||
IconSun,
|
||||
} from '@tabler/icons-react'
|
||||
import { useSearch } from '@/context/search-context'
|
||||
import { useTheme } from '@/context/theme-context'
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import { useSidebarData } from './layout/data/sidebar-data'
|
||||
import { ScrollArea } from './ui/scroll-area'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function CommandMenu() {
|
||||
const navigate = useNavigate()
|
||||
const { setTheme } = useTheme()
|
||||
const { open, setOpen } = useSearch()
|
||||
const sidebarData = useSidebarData()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
},
|
||||
[setOpen]
|
||||
)
|
||||
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder={t('command.typeCommandOrSearch')} />
|
||||
<CommandList>
|
||||
<ScrollArea type='hover' className='h-72 pr-1'>
|
||||
<CommandEmpty>{t('command.noResultsFound')}</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem, i) => {
|
||||
if (navItem.url)
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${navItem.url}-${i}`}
|
||||
value={navItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: navItem.url }))
|
||||
}}
|
||||
>
|
||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
||||
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
|
||||
</div>
|
||||
{navItem.title}
|
||||
</CommandItem>
|
||||
)
|
||||
|
||||
return navItem.items?.map((subItem, i) => (
|
||||
<CommandItem
|
||||
key={`${subItem.url}-${i}`}
|
||||
value={subItem.title}
|
||||
onSelect={() => {
|
||||
runCommand(() => navigate({ to: subItem.url }))
|
||||
}}
|
||||
>
|
||||
<div className='mr-2 flex h-4 w-4 items-center justify-center'>
|
||||
<IconArrowRightDashed className='size-2 text-muted-foreground/80' />
|
||||
</div>
|
||||
{subItem.title}
|
||||
</CommandItem>
|
||||
))
|
||||
})}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading={t('command.theme')}>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<IconSun /> <span>{t('command.light')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
||||
<IconMoon className='scale-90' />
|
||||
<span>{t('command.dark')}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
//
|
||||
// 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 { useEffect, useState } from 'react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import AceEditor from './ace-editor';
|
||||
|
||||
interface CodeEditorWithDraftProps {
|
||||
value: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
localStorageKey: string;
|
||||
mode?: 'handlebars' | 'json' | 'markdown' | 'python';
|
||||
theme?: 'kuroir' | 'monokai';
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CodeEditorWithDraft({
|
||||
value,
|
||||
onChange,
|
||||
localStorageKey,
|
||||
mode,
|
||||
theme,
|
||||
placeholder,
|
||||
className,
|
||||
}: CodeEditorWithDraftProps) {
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if (saved && !value) {
|
||||
setHasDraft(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const save = () => {
|
||||
if (value) {
|
||||
localStorage.setItem(localStorageKey, value);
|
||||
}
|
||||
};
|
||||
const handle = setInterval(save, 2000);
|
||||
return () => {
|
||||
clearInterval(handle);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleRestore = () => {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if (saved) {
|
||||
onChange(saved);
|
||||
setHasDraft(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = () => {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
setHasDraft(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasDraft && (
|
||||
<Alert variant="destructive" className="mt-2 flex items-start gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600 mt-1" />
|
||||
<div>
|
||||
<AlertTitle>Unsaved draft detected</AlertTitle>
|
||||
<AlertDescription>
|
||||
A draft was found in your browser. Would you like to restore it?
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Button size="sm" onClick={handleRestore}>
|
||||
Restore
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={handleDiscard}>
|
||||
Discard
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
<AceEditor
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
mode={mode}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Languages } from 'lucide-react'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'ar', label: 'العربية' },
|
||||
@@ -59,7 +59,7 @@ export function LanguageSwitch() {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Languages className="h-4 w-4" />
|
||||
<Globe/>
|
||||
<span className="sr-only">Change language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -27,7 +27,6 @@ import { LanguageSwitch } from "../language-switch";
|
||||
export const FixedHeader = () => {
|
||||
return (
|
||||
<Header fixed>
|
||||
{/* <Search /> */}
|
||||
<div className='ml-auto flex items-center space-x-4'>
|
||||
<NotificationPopover />
|
||||
<GithubLinkButton />
|
||||
|
||||
@@ -17,32 +17,47 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
interface GithubLinkButtonProps {
|
||||
/** GitHub repository or profile URL */
|
||||
href?: string;
|
||||
/** Icon size (default: 20) */
|
||||
repo?: string;
|
||||
size?: number;
|
||||
/** Optional tooltip title */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const GithubLinkButton: React.FC<GithubLinkButtonProps> = ({
|
||||
href = "https://github.com/rustmailer/bichon",
|
||||
size = 20,
|
||||
repo = "rustmailer/bichon",
|
||||
size = 18,
|
||||
title = "View on GitHub",
|
||||
}) => {
|
||||
const [stars, setStars] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`https://api.github.com/repos/${repo}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setStars(data.stargazers_count))
|
||||
.catch(() => { });
|
||||
}, [repo]);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={title}
|
||||
className="inline-flex items-center justify-center rounded-full p-2 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-xs font-medium"
|
||||
>
|
||||
<GitHubLogoIcon className="w-5 h-5" style={{ width: size, height: size }} />
|
||||
<GitHubLogoIcon style={{ width: size, height: size }} />
|
||||
{stars !== null && (
|
||||
<>
|
||||
<Star className="h-3 w-3 fill-current" />
|
||||
<span>{stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : stars}</span>
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
|
||||
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
||||
@@ -58,7 +57,6 @@ export const Header = ({
|
||||
{...props}
|
||||
>
|
||||
<SidebarTrigger variant='outline' className='scale-125 sm:scale-100' />
|
||||
<Separator orientation='vertical' className='h-6' />
|
||||
{children}
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
//
|
||||
// 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 {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className='ml-auto size-4' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg'
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='p-0 font-normal'>
|
||||
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { get_notifications } from "@/api/system/api";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Release {
|
||||
tag_name: string;
|
||||
@@ -58,6 +59,9 @@ export function NotificationPopover() {
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
});
|
||||
|
||||
|
||||
const {t} = useTranslation();
|
||||
|
||||
const activeNotifications = useMemo((): ActiveNotification[] => {
|
||||
if (!data) return [];
|
||||
|
||||
@@ -107,8 +111,8 @@ export function NotificationPopover() {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[32rem] p-0" align="end">
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-medium">
|
||||
System Notifications
|
||||
<h4 className="font-medium text-sm">
|
||||
{t('system.notifications')}
|
||||
{showNotificationBadge && ` (${activeNotifications.length})`}
|
||||
</h4>
|
||||
</div>
|
||||
@@ -144,7 +148,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{data.tag_name}
|
||||
</h3>
|
||||
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
|
||||
@@ -156,7 +160,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-xs dark:prose-invert max-w-none text-sm">
|
||||
<div className="prose prose-xs dark:prose-invert max-w-none text-xs">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{data.body}
|
||||
</ReactMarkdown>
|
||||
@@ -168,7 +172,7 @@ function ReleaseNotificationView({ data }: { data: Release }) {
|
||||
href={data.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline inline-flex items-center"
|
||||
className="text-xs text-primary hover:underline inline-flex items-center"
|
||||
>
|
||||
View full release notes <ExternalLinkIcon className="ml-1 h-3 w-3" />
|
||||
</a>
|
||||
|
||||
@@ -26,8 +26,7 @@ import { Button } from '@/components/ui/button'
|
||||
export function ThemeSwitch() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
/* Update theme-color meta tag
|
||||
* when theme is updated */
|
||||
|
||||
useEffect(() => {
|
||||
const themeColor = theme === 'dark' ? '#020817' : '#fff'
|
||||
const metaThemeColor = document.querySelector("meta[name='theme-color']")
|
||||
@@ -39,7 +38,7 @@ export function ThemeSwitch() {
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<Button variant='ghost' size='icon' className='scale-95 rounded-full' onClick={toggleTheme}>
|
||||
<Button variant='ghost' size='icon' className='scale-95' onClick={toggleTheme}>
|
||||
<IconSun className='size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
|
||||
<IconMoon className='absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
|
||||
<span className='sr-only'>Toggle theme</span>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
|
||||
import React from 'react'
|
||||
import { CommandMenu } from '@/components/command-menu'
|
||||
|
||||
interface SearchContextType {
|
||||
open: boolean
|
||||
@@ -48,7 +47,6 @@ export function SearchProvider({ children }: Props) {
|
||||
return (
|
||||
<SearchContext.Provider value={{ open, setOpen }}>
|
||||
{children}
|
||||
<CommandMenu />
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "دور {{type}}",
|
||||
"title": "الصلاحيات: {{name}}",
|
||||
"unknown": "دور غير معروف",
|
||||
"view_permissions": "عرض التفاصيل"
|
||||
"view_permissions": "التفاصيل"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "لم تقم بإنشاء أي أدوار مخصصة بعد.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "المستخدم الجذر",
|
||||
"selectAll": "تحديد الكل",
|
||||
"sidebar": {
|
||||
"access": "صلاحيات الوصول",
|
||||
"apiTokens": "رموز API",
|
||||
"configurations": "تكوينات النظام",
|
||||
"profile": "الملف الشخصي",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "هل أنت متأكد أنك تريد تسجيل الخروج؟ ستحتاج إلى تسجيل الدخول مرة أخرى للوصول إلى حسابك.",
|
||||
"title": "تسجيل الخروج"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "إشعارات النظام"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "يتم تهيئة هذه المعلمات عبر علامات CLI أو متغيرات البيئة. وهي حاليًا للقراءة فقط لضمان اتساق البيئة.",
|
||||
"pageTitle": "تكوينات مستوى النظام",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} rolle",
|
||||
"title": "Rettigheder: {{name}}",
|
||||
"unknown": "Ukendt rolle",
|
||||
"view_permissions": "Se detaljer"
|
||||
"view_permissions": "Detaljer"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Du har ikke oprettet nogen brugerdefinerede roller endnu.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Vælg alle",
|
||||
"sidebar": {
|
||||
"access": "Adgang",
|
||||
"apiTokens": "API-tokens",
|
||||
"configurations": "Systemkonfigurationer",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Er du sikker på, at du vil logge ud? Du skal logge ind igen for at få adgang til din konto.",
|
||||
"title": "Log ud"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Systemmeddelelser"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Disse parametre initialiseres via CLI-flag eller miljøvariabler. De er skrivebeskyttede.",
|
||||
"pageTitle": "Systemkonfigurationer",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}}-Rolle",
|
||||
"title": "Berechtigungen: {{name}}",
|
||||
"unknown": "Unbekannte Rolle",
|
||||
"view_permissions": "Details anzeigen"
|
||||
"view_permissions": "Details"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Sie haben noch keine benutzerdefinierten Rollen erstellt.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root-Benutzer",
|
||||
"selectAll": "Alle auswählen",
|
||||
"sidebar": {
|
||||
"access": "Zugriffsrechte",
|
||||
"apiTokens": "API-Token",
|
||||
"configurations": "Systemkonfigurationen",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Möchten Sie sich wirklich abmelden? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.",
|
||||
"title": "Abmelden"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Systembenachrichtigungen"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Diese Parameter werden über CLI-Flags oder Umgebungsvariablen initialisiert. Sie sind schreibgeschützt, um die Umgebungskonsistenz zu gewährleisten.",
|
||||
"pageTitle": "Systemkonfigurationen",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} Role",
|
||||
"title": "Role Permissions: {{name}}",
|
||||
"unknown": "Unknown Role",
|
||||
"view_permissions": "Permission Details"
|
||||
"view_permissions": "Details"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "You haven't created any custom roles yet.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Select all",
|
||||
"sidebar": {
|
||||
"access": "Access Permissions",
|
||||
"apiTokens": "API Tokens",
|
||||
"configurations": "System Configurations",
|
||||
"profile": "Profile",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Are you sure you want to sign out? You will need to sign in again to access your account.",
|
||||
"title": "Sign out"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "System Notifications"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "These parameters are initialized via CLI flags or Environment Variables. They are currently read-only to ensure environment consistency.",
|
||||
"pageTitle": "System-level Configurations",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Rol {{type}}",
|
||||
"title": "Permisos: {{name}}",
|
||||
"unknown": "Rol desconocido",
|
||||
"view_permissions": "Ver detalles"
|
||||
"view_permissions": "Detalles"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Aún no ha creado ningún rol personalizado.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Usuario raíz",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"sidebar": {
|
||||
"access": "Permisos de acceso",
|
||||
"apiTokens": "Tokens API",
|
||||
"configurations": "Configuraciones del sistema",
|
||||
"profile": "Perfil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "¿Está seguro de que desea cerrar sesión? Necesitará iniciar sesión nuevamente para acceder a su cuenta.",
|
||||
"title": "Cerrar sesión"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Notificaciones del sistema"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Estos parámetros se inicializan mediante indicadores CLI o variables de entorno. Actualmente son de solo lectura.",
|
||||
"pageTitle": "Configuraciones a nivel de sistema",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}}-rooli",
|
||||
"title": "Oikeudet: {{name}}",
|
||||
"unknown": "Tuntematon rooli",
|
||||
"view_permissions": "Katso yksityiskohdat"
|
||||
"view_permissions": "Tiedot"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Et ole vielä luonut mukautettuja rooleja.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Pääkäyttäjä",
|
||||
"selectAll": "Valitse kaikki",
|
||||
"sidebar": {
|
||||
"access": "Käyttöoikeudet",
|
||||
"apiTokens": "API-tunnukset",
|
||||
"configurations": "Järjestelmäasetukset",
|
||||
"profile": "Profiili",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Haluatko varmasti kirjautua ulos? Sinun täytyy kirjautua uudelleen päästäksesi tilillesi.",
|
||||
"title": "Kirjaudu ulos"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Järjestelmäilmoitukset"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Nämä parametrit alustetaan CLI-lippujen tai ympäristömuuttujien kautta. Ne ovat vain luku -tilassa.",
|
||||
"pageTitle": "Järjestelmätason konfiguraatiot",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Rôle {{type}}",
|
||||
"title": "Permissions : {{name}}",
|
||||
"unknown": "Rôle inconnu",
|
||||
"view_permissions": "Détails des permissions"
|
||||
"view_permissions": "Détails"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Vous n'avez pas encore créé de rôles personnalisés.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"sidebar": {
|
||||
"access": "Droits d'accès",
|
||||
"apiTokens": "Jetons API",
|
||||
"configurations": "Configurations système",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||
"title": "Déconnexion"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Notifications système"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Ces paramètres sont initialisés via des drapeaux CLI ou des variables d'environnement. Ils sont en lecture seule pour garantir la cohérence de l'environnement.",
|
||||
"pageTitle": "Configurations au niveau système",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Ruolo {{type}}",
|
||||
"title": "Permessi: {{name}}",
|
||||
"unknown": "Ruolo sconosciuto",
|
||||
"view_permissions": "Dettagli permessi"
|
||||
"view_permissions": "Dettagli"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Non hai ancora creato alcun ruolo personalizzato.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Seleziona Tutto",
|
||||
"sidebar": {
|
||||
"access": "Permessi di accesso",
|
||||
"apiTokens": "Token API",
|
||||
"configurations": "Configurazioni di sistema",
|
||||
"profile": "Profilo",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Sei sicuro di voler disconnetterti? Dovrai accedere di nuovo per usare il tuo account.",
|
||||
"title": "Disconnetti"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Notifiche di sistema"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Questi parametri sono inizializzati tramite flag CLI o variabili d'ambiente. Sono di sola lettura per garantire la coerenza.",
|
||||
"pageTitle": "Configurazioni a livello di sistema",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} ロール",
|
||||
"title": "ロール権限: {{name}}",
|
||||
"unknown": "不明なロール",
|
||||
"view_permissions": "権限の詳細"
|
||||
"view_permissions": "詳細"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "カスタムロールがまだ作成されていません。",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "ルート",
|
||||
"selectAll": "すべて選択",
|
||||
"sidebar": {
|
||||
"access": "アクセス権限",
|
||||
"apiTokens": "APIトークン",
|
||||
"configurations": "システム設定",
|
||||
"profile": "プロフィール",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "本当にサインアウトしますか? アカウントにアクセスするには再度サインインが必要です。",
|
||||
"title": "サインアウト"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "システム通知"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "これらのパラメータは、CLIフラグまたは環境変数を介して初期化されます。環境の一貫性を確保するため、現在は読み取り専用です。",
|
||||
"pageTitle": "システムレベルの設定",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} 역할",
|
||||
"title": "역할 권한: {{name}}",
|
||||
"unknown": "알 수 없는 역할",
|
||||
"view_permissions": "권한 상세 정보"
|
||||
"view_permissions": "상세 정보"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "사용자 정의 역할이 아직 생성되지 않았습니다.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "루트",
|
||||
"selectAll": "모두 선택",
|
||||
"sidebar": {
|
||||
"access": "액세스 권한",
|
||||
"apiTokens": "API 토큰",
|
||||
"configurations": "시스템 설정",
|
||||
"profile": "프로필",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "로그아웃하시겠습니까? 계정에 접근하려면 다시 로그인해야 합니다.",
|
||||
"title": "로그아웃"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "시스템 알림"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "이 매개변수는 CLI 플래그 또는 환경 변수를 통해 초기화됩니다. 환경의 일관성을 위해 현재 읽기 전용입니다.",
|
||||
"pageTitle": "시스템 레벨 구성",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} rol",
|
||||
"title": "Permissies: {{name}}",
|
||||
"unknown": "Onbekende rol",
|
||||
"view_permissions": "Details bekijken"
|
||||
"view_permissions": "Details"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "U heeft nog geen aangepaste rollen gemaakt.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Alles selecteren",
|
||||
"sidebar": {
|
||||
"access": "Toegangsrechten",
|
||||
"apiTokens": "API-tokens",
|
||||
"configurations": "Systeemconfiguraties",
|
||||
"profile": "Profiel",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Weet je zeker dat je wilt uitloggen? Je moet opnieuw inloggen om toegang te krijgen tot je account.",
|
||||
"title": "Uitloggen"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Systeemmeldingen"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Deze parameters worden geïnitialiseerd via CLI-flags of omgevingsvariabelen. Ze zijn alleen-lezen.",
|
||||
"pageTitle": "Systeemconfiguraties",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}}-rolle",
|
||||
"title": "Tillatelser: {{name}}",
|
||||
"unknown": "Ukjent rolle",
|
||||
"view_permissions": "Se detaljer"
|
||||
"view_permissions": "Detaljer"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Du har ikke opprettet noen egendefinerte roller ennå.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Velg alle",
|
||||
"sidebar": {
|
||||
"access": "Tilgang",
|
||||
"apiTokens": "API-tokener",
|
||||
"configurations": "Systemkonfigurasjoner",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Er du sikker på at du vil logge ut? Du må logge inn igjen for å få tilgang til kontoen din.",
|
||||
"title": "Logg ut"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Systemvarsler"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Disse parametrene initialiseres via CLI-flagg eller miljøvariabler. De er skrivebeskyttede.",
|
||||
"pageTitle": "Systemkonfigurasjoner",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Rola {{type}}",
|
||||
"title": "Uprawnienia: {{name}}",
|
||||
"unknown": "Nieznana rola",
|
||||
"view_permissions": "Zobacz szczegóły"
|
||||
"view_permissions": "Szczegóły"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Nie utworzyłeś jeszcze żadnych własnych ról.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Zaznacz wszystko",
|
||||
"sidebar": {
|
||||
"access": "Uprawnienia dostępu",
|
||||
"apiTokens": "Tokeny API",
|
||||
"configurations": "Konfiguracja systemu",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Czy na pewno chcesz się wylogować? Aby uzyskać dostęp do konta, będziesz musiał zalogować się ponownie.",
|
||||
"title": "Wyloguj się"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Powiadomienia systemowe"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Parametry te są inicjowane za pomocą flag CLI lub zmiennych środowiskowych. Są one obecnie tylko do odczytu.",
|
||||
"pageTitle": "Konfiguracje na poziomie systemu",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Função {{type}}",
|
||||
"title": "Permissões: {{name}}",
|
||||
"unknown": "Função desconhecida",
|
||||
"view_permissions": "Ver detalhes"
|
||||
"view_permissions": "Detalhes"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Você ainda não criou funções personalizadas.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Selecionar Todos",
|
||||
"sidebar": {
|
||||
"access": "Permissões de acesso",
|
||||
"apiTokens": "Tokens API",
|
||||
"configurations": "Configurações do sistema",
|
||||
"profile": "Perfil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Tem certeza de que deseja sair? Você precisará entrar novamente para acessar sua conta.",
|
||||
"title": "Sair"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Notificações do sistema"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Estes parâmetros são inicializados via flags CLI ou variáveis de ambiente. Estão em modo somente leitura.",
|
||||
"pageTitle": "Configurações a nível de sistema",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "Роль: {{type}}",
|
||||
"title": "Права роли: {{name}}",
|
||||
"unknown": "Неизвестная роль",
|
||||
"view_permissions": "Детали прав"
|
||||
"view_permissions": "Подробнее"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Вы еще не создали ни одной кастомной роли.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Выбрать все",
|
||||
"sidebar": {
|
||||
"access": "Права доступа",
|
||||
"apiTokens": "API-токены",
|
||||
"configurations": "Системные конфигурации",
|
||||
"profile": "Профиль",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Вы уверены, что хотите выйти? Вам нужно будет снова войти, чтобы получить доступ к аккаунту.",
|
||||
"title": "Выйти"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Системные уведомления"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Эти параметры инициализируются через флаги CLI или переменные окружения. Они доступны только для чтения.",
|
||||
"pageTitle": "Конфигурации системного уровня",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} Roll",
|
||||
"title": "Rollbehörigheter: {{name}}",
|
||||
"unknown": "Okänd roll",
|
||||
"view_permissions": "Visa behörigheter"
|
||||
"view_permissions": "Detaljer"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "Du har inte skapat några anpassade roller än.",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "Root",
|
||||
"selectAll": "Välj alla",
|
||||
"sidebar": {
|
||||
"access": "Behörigheter",
|
||||
"apiTokens": "API-tokens",
|
||||
"configurations": "Systemkonfigurationer",
|
||||
"profile": "Profil",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "Är du säker på att du vill logga ut? Du måste logga in igen för att få åtkomst till ditt konto.",
|
||||
"title": "Logga ut"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "Systemaviseringar"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "Dessa parametrar initieras via CLI-flaggor eller miljövariabler. De är skrivskyddade.",
|
||||
"pageTitle": "Systemkonfigurationer",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} 角色",
|
||||
"title": "角色權限: {{name}}",
|
||||
"unknown": "未知角色",
|
||||
"view_permissions": "權限詳情"
|
||||
"view_permissions": "詳情"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "您尚未建立任何自定義角色。點擊下方按鈕開始新增新角色。",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "根目錄",
|
||||
"selectAll": "全選",
|
||||
"sidebar": {
|
||||
"access": "訪問權限",
|
||||
"apiTokens": "API 權杖",
|
||||
"configurations": "系統配置",
|
||||
"profile": "個人資料",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "您確定要登出嗎?您需要重新登入才能存取帳戶。",
|
||||
"title": "登出"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "系統通知"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "這些參數透過 CLI 標誌或環境變數初始化。它們目前為唯讀,以確保環境一致性。",
|
||||
"pageTitle": "系統級配置",
|
||||
|
||||
@@ -915,7 +915,7 @@
|
||||
"role_badge": "{{type}} 角色",
|
||||
"title": "角色权限: {{name}}",
|
||||
"unknown": "未知角色",
|
||||
"view_permissions": "权限详情"
|
||||
"view_permissions": "详情"
|
||||
},
|
||||
"empty": {
|
||||
"desc": "您尚未创建任何自定义角色。点击下方按钮开始添加新角色。",
|
||||
@@ -1272,6 +1272,7 @@
|
||||
"rootTitle": "管理员账户",
|
||||
"selectAll": "全选",
|
||||
"sidebar": {
|
||||
"access": "访问权限",
|
||||
"apiTokens": "API 令牌",
|
||||
"configurations": "系统配置",
|
||||
"profile": "个人资料",
|
||||
@@ -1300,6 +1301,9 @@
|
||||
"desc": "您确定要退出登录吗?您需要重新登录才能访问账户。",
|
||||
"title": "退出登录"
|
||||
},
|
||||
"system": {
|
||||
"notifications": "系统通知"
|
||||
},
|
||||
"systemConfig": {
|
||||
"pageDescription": "这些参数通过 CLI 标志或环境变量初始化。它们当前为只读,以确保环境一致性。",
|
||||
"pageTitle": "系统级配置",
|
||||
|
||||
@@ -72,6 +72,9 @@ const AuthenticatedSettingsAppearanceLazyImport = createFileRoute(
|
||||
const AuthenticatedSettingsApiTokensLazyImport = createFileRoute(
|
||||
'/_authenticated/settings/api-tokens',
|
||||
)()
|
||||
const AuthenticatedSettingsAccessLazyImport = createFileRoute(
|
||||
'/_authenticated/settings/access',
|
||||
)()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@@ -298,6 +301,15 @@ const AuthenticatedSettingsApiTokensLazyRoute =
|
||||
),
|
||||
)
|
||||
|
||||
const AuthenticatedSettingsAccessLazyRoute =
|
||||
AuthenticatedSettingsAccessLazyImport.update({
|
||||
id: '/access',
|
||||
path: '/access',
|
||||
getParentRoute: () => AuthenticatedSettingsRouteLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_authenticated/settings/access.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@@ -379,6 +391,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedIndexImport
|
||||
parentRoute: typeof AuthenticatedRouteImport
|
||||
}
|
||||
'/_authenticated/settings/access': {
|
||||
id: '/_authenticated/settings/access'
|
||||
path: '/access'
|
||||
fullPath: '/settings/access'
|
||||
preLoaderRoute: typeof AuthenticatedSettingsAccessLazyImport
|
||||
parentRoute: typeof AuthenticatedSettingsRouteLazyImport
|
||||
}
|
||||
'/_authenticated/settings/api-tokens': {
|
||||
id: '/_authenticated/settings/api-tokens'
|
||||
path: '/api-tokens'
|
||||
@@ -490,6 +509,7 @@ declare module '@tanstack/react-router' {
|
||||
// Create and export the route tree
|
||||
|
||||
interface AuthenticatedSettingsRouteLazyRouteChildren {
|
||||
AuthenticatedSettingsAccessLazyRoute: typeof AuthenticatedSettingsAccessLazyRoute
|
||||
AuthenticatedSettingsApiTokensLazyRoute: typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
AuthenticatedSettingsAppearanceLazyRoute: typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
AuthenticatedSettingsConfigurationsLazyRoute: typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
@@ -500,6 +520,7 @@ interface AuthenticatedSettingsRouteLazyRouteChildren {
|
||||
|
||||
const AuthenticatedSettingsRouteLazyRouteChildren: AuthenticatedSettingsRouteLazyRouteChildren =
|
||||
{
|
||||
AuthenticatedSettingsAccessLazyRoute: AuthenticatedSettingsAccessLazyRoute,
|
||||
AuthenticatedSettingsApiTokensLazyRoute:
|
||||
AuthenticatedSettingsApiTokensLazyRoute,
|
||||
AuthenticatedSettingsAppearanceLazyRoute:
|
||||
@@ -576,6 +597,7 @@ export interface FileRoutesByFullPath {
|
||||
'/404': typeof errors404LazyRoute
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
@@ -601,6 +623,7 @@ export interface FileRoutesByTo {
|
||||
'/404': typeof errors404LazyRoute
|
||||
'/503': typeof errors503LazyRoute
|
||||
'/': typeof AuthenticatedIndexRoute
|
||||
'/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
'/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
@@ -631,6 +654,7 @@ export interface FileRoutesById {
|
||||
'/(errors)/500': typeof errors500LazyRoute
|
||||
'/(errors)/503': typeof errors503LazyRoute
|
||||
'/_authenticated/': typeof AuthenticatedIndexRoute
|
||||
'/_authenticated/settings/access': typeof AuthenticatedSettingsAccessLazyRoute
|
||||
'/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute
|
||||
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute
|
||||
'/_authenticated/settings/configurations': typeof AuthenticatedSettingsConfigurationsLazyRoute
|
||||
@@ -661,6 +685,7 @@ export interface FileRouteTypes {
|
||||
| '/404'
|
||||
| '/503'
|
||||
| '/'
|
||||
| '/settings/access'
|
||||
| '/settings/api-tokens'
|
||||
| '/settings/appearance'
|
||||
| '/settings/configurations'
|
||||
@@ -685,6 +710,7 @@ export interface FileRouteTypes {
|
||||
| '/404'
|
||||
| '/503'
|
||||
| '/'
|
||||
| '/settings/access'
|
||||
| '/settings/api-tokens'
|
||||
| '/settings/appearance'
|
||||
| '/settings/configurations'
|
||||
@@ -713,6 +739,7 @@ export interface FileRouteTypes {
|
||||
| '/(errors)/500'
|
||||
| '/(errors)/503'
|
||||
| '/_authenticated/'
|
||||
| '/_authenticated/settings/access'
|
||||
| '/_authenticated/settings/api-tokens'
|
||||
| '/_authenticated/settings/appearance'
|
||||
| '/_authenticated/settings/configurations'
|
||||
@@ -797,6 +824,7 @@ export const routeTree = rootRoute
|
||||
"filePath": "_authenticated/settings/route.lazy.tsx",
|
||||
"parent": "/_authenticated",
|
||||
"children": [
|
||||
"/_authenticated/settings/access",
|
||||
"/_authenticated/settings/api-tokens",
|
||||
"/_authenticated/settings/appearance",
|
||||
"/_authenticated/settings/configurations",
|
||||
@@ -833,6 +861,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "_authenticated/index.tsx",
|
||||
"parent": "/_authenticated"
|
||||
},
|
||||
"/_authenticated/settings/access": {
|
||||
"filePath": "_authenticated/settings/access.lazy.tsx",
|
||||
"parent": "/_authenticated/settings"
|
||||
},
|
||||
"/_authenticated/settings/api-tokens": {
|
||||
"filePath": "_authenticated/settings/api-tokens.lazy.tsx",
|
||||
"parent": "/_authenticated/settings"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AccountAccessList } from '@/features/settings/access'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createLazyFileRoute('/_authenticated/settings/access')({
|
||||
component: AccountAccessList,
|
||||
})
|
||||
Reference in New Issue
Block a user