Files
bichon/web/src/features/accounts/index.tsx
T

222 lines
8.4 KiB
TypeScript

//
// 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 useDialogState from '@/hooks/use-dialog-state'
import { Button } from '@/components/ui/button'
import { Main } from '@/components/layout/main'
import { AccountActionDialog } from './components/action-dialog'
import { useColumns } from './components/columns'
import { AccountDeleteDialog } from './components/delete-dialog'
import { AccountTable } from './components/table'
import AccountProvider, {
type AccountDialogType,
} from './context'
import { Plus } from 'lucide-react'
import Logo from '@/assets/logo.svg'
import { AccountDetailDrawer } from './components/account-detail'
import { AccountModel, list_accounts } from '@/api/account/api'
import { TableSkeleton } from '@/components/table-skeleton'
import { useQuery } from '@tanstack/react-query'
import { OAuth2TokensDialog } from './components/oauth2-tokens'
import { RunningStateDialog } from './components/running-state-dialog'
import { FixedHeader } from '@/components/layout/fixed-header'
import { DownloadFoldersDialog } from './components/download-folders'
import { NoSyncAccountDialog } from './components/nosync-dialog'
import { useTranslation } from 'react-i18next'
import { AccountAccessAssignmentDialog } from './components/access-assignment-dialog'
import { useCurrentUser } from '@/hooks/use-current-user'
import { AddAccountDialog } from './components/add-account-dialog'
export default function Accounts() {
const { t } = useTranslation()
const columns = useColumns()
// Dialog states
const [currentRow, setCurrentRow] = useState<AccountModel | null>(null)
const [open, setOpen] = useDialogState<AccountDialogType>(null)
const { require_any_permission } = useCurrentUser()
const { data: accountList, isLoading } = useQuery({
queryKey: ['account-list'],
queryFn: list_accounts,
refetchInterval: (query) => {
const items = (query.state.data as { items?: { deleting?: boolean }[] })?.items;
return items?.some((item) => item.deleting) ? 5000 : false;
},
})
const hasAccounts = accountList != null && accountList.items.length > 0;
return (
<AccountProvider value={{ open, setOpen, currentRow, setCurrentRow }}>
<FixedHeader />
<Main>
<div className="mx-auto w-full max-w-[108rem] px-4">
<div className='mb-2 flex items-center justify-between flex-wrap gap-x-4 gap-y-2'>
<div>
<h2 className='text-2xl font-bold tracking-tight'>{t('accounts.title')}</h2>
<p className='text-muted-foreground'>
{t('accounts.description')}
</p>
</div>
{require_any_permission(['system:root', 'account:create']) && <div className="flex gap-2">
<div className="flex rounded-md shadow-sm">
<Button
onClick={() => setOpen("add")}
className="border-r-0"
>
<Plus className="h-4 w-4" />
{t('accounts.add')}
</Button>
</div>
</div>}
</div>
<div className='flex-1 overflow-auto py-1 flex-row lg:space-x-12 space-y-0'>
{isLoading ? (
<TableSkeleton columns={columns.length} rows={10} />
) : hasAccounts ? (
<AccountTable data={accountList.items} columns={columns} />
) : (
<div className="flex h-[450px] 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('accounts.noAccountConfigurations')}</h3>
<p className="mb-4 mt-2 text-sm text-muted-foreground">
{t('accounts.noAccountConfigurationsDesc')}
</p>
<div className="mt-4 flex flex-col items-center gap-3 sm:flex-row sm:flex-wrap sm:justify-center sm:gap-4">
<Button variant="default" className="w-64" onClick={() => setOpen("add")}>
{t('accounts.add')}
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</Main>
<AddAccountDialog
key='account-add'
open={open === 'add'}
onOpenChange={() => setOpen('add')} />
<AccountActionDialog
key='imap-account-add'
open={open === 'add-imap'}
onOpenChange={() => setOpen('add-imap')}
/>
<NoSyncAccountDialog
key='nosync-account-add'
open={open === 'add-nosync'}
onOpenChange={() => setOpen('add-nosync')}
/>
{currentRow && (
<>
<AccountActionDialog
key={`imap-account-edit-${currentRow.id}`}
open={open === 'edit-imap'}
onOpenChange={() => {
setOpen('edit-imap')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>
<NoSyncAccountDialog
key={`nosync-account-edit-${currentRow.id}`}
open={open === 'edit-nosync'}
onOpenChange={() => {
setOpen('edit-nosync')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>
{require_any_permission(['system:root', 'account:read_details'], currentRow.id) && <RunningStateDialog
key='running-state'
open={open === 'running-state'}
onOpenChange={() => {
setOpen('running-state')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>}
<AccountDeleteDialog
key={`account-delete-${currentRow.id}`}
open={open === 'delete'}
onOpenChange={() => {
setOpen('delete')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>
<DownloadFoldersDialog
key={`sync-folders-${currentRow.id}`}
open={open === 'sync-folders'}
onOpenChange={() => {
setOpen('sync-folders')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>
{require_any_permission(['system:root', 'account:manage'], currentRow.id) && <AccountAccessAssignmentDialog
key={`access-assign-${currentRow.id}`}
open={open === 'access-assign'}
onOpenChange={() => {
setOpen('access-assign')
setTimeout(() => {
setCurrentRow(null)
}, 500)
}}
currentRow={currentRow}
/>}
<AccountDetailDrawer
open={open === 'detail'}
onOpenChange={() => setOpen('detail')}
currentRow={currentRow}
/>
{require_any_permission(['system:root', 'account:manage'], currentRow.id) && <OAuth2TokensDialog open={open === 'oauth2'}
onOpenChange={() => setOpen('oauth2')}
currentRow={currentRow}
/>}
</>
)}
</AccountProvider>
)
}