mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: add multi-user support and role-based access control #31
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user