feat: add multi-user support and role-based access control #31

This commit is contained in:
rustmailer
2025-12-26 14:27:04 +08:00
parent 1e2f526a07
commit 4af5176b65
181 changed files with 23745 additions and 3187 deletions
+26 -1
View File
@@ -28,6 +28,7 @@ import { toast } from '@/hooks/use-toast';
import { EmailEnvelope } from '@/api';
import { validateTag } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
interface Props {
open: boolean
@@ -37,6 +38,7 @@ interface Props {
export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
const { tags: availableTags } = useAvailableTags();
const queryClient = useQueryClient();
const { mutate, isPending } = useUpdateTags();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
@@ -74,6 +76,26 @@ export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
};
const handleSave = () => {
if (inputValue.trim()) {
const normalized = inputValue.toLowerCase().trim();
const result = validateTag(normalized);
if (!result.valid) {
toast({
title: t('search.addTags.invalidTitle'),
description: result.error,
variant: 'destructive',
});
return;
}
if (!selectedTags.includes(normalized)) {
setSelectedTags(prev => [...prev, normalized]);
}
setInputValue('');
}
const updates = {
[currentEnvelope.account_id]: [currentEnvelope.id],
};
@@ -81,7 +103,9 @@ export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
mutate(
{
updates,
tags: selectedTags
tags: inputValue.trim()
? [...selectedTags, inputValue.toLowerCase().trim()]
: selectedTags,
},
{
onSuccess: () => {
@@ -94,6 +118,7 @@ export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
</div>
),
});
queryClient.invalidateQueries({ queryKey: ['all-tags'] });
onOpenChange(false);
},
onError: (error: any) => {
-3
View File
@@ -146,9 +146,6 @@ export function MailBulkActions({ children }: MailBulkActionsProps) {
<span className="sr-only">{t('search.bulkActions.clear')}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{t('search.bulkActions.clearWithKey', { key: 'Escape' })}
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-5" />
+14 -9
View File
@@ -37,6 +37,7 @@ import { Button } from '@/components/ui/button';
import { EnvelopeTags } from './tag-facet';
import { EditTagsDialog } from './add-tag-dialog';
import { useTranslation } from 'react-i18next';
import Logo from '@/assets/logo.svg'
export default function Search() {
const { t } = useTranslation()
@@ -134,16 +135,20 @@ export default function Search() {
</Card>
)}
{total === 0 && <div className="text-center py-12 space-y-4">
<div className="bg-muted/50 border-2 border-dashed rounded-xl w-24 h-24 mx-auto flex items-center justify-center">
<SearchIcon className="w-10 h-10 text-muted-foreground" />
{total === 0 && <div className="flex h-[750px] shrink-0 items-center justify-center rounded-md border border-dashed">
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
<img
src={Logo}
className="max-h-[100px] w-auto opacity-20 saturate-0 transition-all duration-300 hover:opacity-100 hover:saturate-100 object-contain"
alt="Bichon Logo"
/>
<h3 className="mt-4 text-lg font-semibold">{t('search.noEmailsFound')}</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
{Object.keys(filter).length === 0
? t('search.startSearching')
: t('search.adjustSearch')}
</p>
</div>
<h3 className="text-lg font-medium">{t('search.noEmailsFound')}</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
{Object.keys(filter).length === 0
? t('search.startSearching')
: t('search.adjustSearch')}
</p>
</div>}
{total > 0 && <ScrollArea className='h-[40rem] w-full pr-4 -mr-4 py-1'>
<MailList