mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: Add sync_batch_size to allow users to customize the synchronization batch size, and introduce date_before to support semantics such as downloading emails from more than one year ago. #24 #58
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
|
||||
|
||||
import axiosInstance from "@/api/axiosInstance";
|
||||
import { AccountModel } from "@/features/accounts/data/schema";
|
||||
import { PaginatedResponse } from "..";
|
||||
|
||||
export interface MinimalAccount {
|
||||
@@ -56,6 +55,59 @@ export interface MailboxBatchProgress {
|
||||
current_batch: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
type Encryption = 'Ssl' | 'StartTls' | 'None';
|
||||
type AuthType = 'Password' | 'OAuth2';
|
||||
type Unit = 'Days' | 'Months' | 'Years';
|
||||
type AccountType = 'IMAP' | 'NoSync';
|
||||
// Interface definitions
|
||||
interface AuthConfig {
|
||||
auth_type: AuthType;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface ImapConfig {
|
||||
host: string;
|
||||
port: number; // integer, 0-65535
|
||||
encryption: Encryption;
|
||||
auth: AuthConfig;
|
||||
use_proxy?: number;
|
||||
}
|
||||
|
||||
interface RelativeDate {
|
||||
unit: Unit;
|
||||
value: number; // integer, minimum 1
|
||||
}
|
||||
|
||||
interface DateSelection {
|
||||
fixed?: string; // format: "YYYY-MM-DD"
|
||||
relative?: RelativeDate;
|
||||
}
|
||||
|
||||
export interface AccountModel {
|
||||
id: number;
|
||||
account_type: AccountType;
|
||||
imap?: ImapConfig;
|
||||
enabled: boolean;
|
||||
name?: string,
|
||||
email: string;
|
||||
capabilities?: string[];
|
||||
date_since?: DateSelection;
|
||||
date_before?: RelativeDate;
|
||||
folder_limit?: number,
|
||||
sync_folders: string[];
|
||||
sync_interval_min?: number;
|
||||
sync_batch_size?: number;
|
||||
created_by: number;
|
||||
created_user_name: string;
|
||||
created_user_email: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
use_proxy?: number
|
||||
use_dangerous: boolean
|
||||
}
|
||||
|
||||
export const account_state = async (account_id: number) => {
|
||||
const response = await axiosInstance.get<AccountRunningState>(`/api/v1/account-state/${account_id}`);
|
||||
return response.data;
|
||||
|
||||
@@ -52,11 +52,9 @@ import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { useRoles } from '@/hooks/use-roles'
|
||||
import { useMinimalUsers } from '@/hooks/use-minimal-users'
|
||||
import { access_assign } from '@/api/account/api'
|
||||
import { access_assign, AccountModel } from '@/api/account/api'
|
||||
|
||||
interface Props {
|
||||
currentRow: AccountModel
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -25,6 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -34,6 +34,30 @@ interface Props {
|
||||
|
||||
export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
|
||||
|
||||
const sinceText = (() => {
|
||||
if (currentRow.date_since?.fixed) {
|
||||
return currentRow.date_since.fixed;
|
||||
}
|
||||
|
||||
if (currentRow.date_since?.relative?.value) {
|
||||
return `${t('accounts.sinceRelativeValue', {
|
||||
value: currentRow.date_since!.relative!.value,
|
||||
unit: t(`accounts.${currentRow.date_since!.relative!.unit!.toLowerCase()}`)
|
||||
})}`;
|
||||
}
|
||||
|
||||
return t('accounts.syncAll');
|
||||
})();
|
||||
|
||||
const hasSince = !!currentRow.date_since;
|
||||
const hasBefore = !!currentRow.date_before?.value;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -77,6 +101,10 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||
<span className="text-muted-foreground">{t('accounts.incrementalSyncInterval')}:</span>
|
||||
<span>{t('accounts.everyMinutes', { minutes: currentRow.sync_interval_min })}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-muted-foreground">{t('accounts.syncBatchSize')}:</span>
|
||||
<span>{currentRow.sync_batch_size}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-muted-foreground">{t('accounts.capabilities')}:</span>
|
||||
<code className="rounded-md bg-muted/50 px-2 py-1 text-sm border overflow-x-auto inline-block">
|
||||
@@ -84,14 +112,33 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-muted-foreground">{t('accounts.dateSelection')}:</span>
|
||||
<span>
|
||||
{currentRow.date_since?.fixed
|
||||
? t('accounts.since') + ' ' + currentRow.date_since.fixed
|
||||
: currentRow.date_since?.relative
|
||||
? t('accounts.recent') + ' ' + currentRow.date_since.relative.value + ' ' + currentRow.date_since.relative.unit
|
||||
: t('accounts.notAvailable')}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{t('accounts.syncScope')}:</span>
|
||||
{hasSince && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.sinceFixed')}:
|
||||
</span>
|
||||
<span className="text-sm">{sinceText}</span>
|
||||
</div>
|
||||
)}
|
||||
{hasBefore && (
|
||||
<div className="flex flex-col border-t pt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.beforeRelative')}:
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{t('accounts.beforeRelativeValue', {
|
||||
value: currentRow.date_before!.value,
|
||||
unit: t(`accounts.${currentRow.date_before!.unit!.toLowerCase()}`)
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasSince && !hasBefore && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('accounts.syncAll')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-muted-foreground">{t('accounts.folderLimit')}:</span>
|
||||
@@ -100,8 +147,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Server Configuration Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('accounts.serverConfiguration')}</CardTitle>
|
||||
@@ -143,8 +188,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sync Folders Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('accounts.syncFoldersTitle')}</CardTitle>
|
||||
|
||||
@@ -16,14 +16,12 @@
|
||||
// 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 { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { AccountModel, ImapConfig } from '../data/schema';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -31,11 +29,12 @@ import Step1 from './step1';
|
||||
import Step2 from './step2';
|
||||
import Step3 from './step3';
|
||||
import Step4 from './step4';
|
||||
import { create_account, autoconfig, update_account } from '@/api/account/api';
|
||||
import { create_account, autoconfig, update_account, AccountModel, ImapConfig } from '@/api/account/api';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ToastAction } from '@/components/ui/toast';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const encryptionSchema = z.union([
|
||||
z.literal('Ssl'),
|
||||
@@ -80,7 +79,7 @@ const getRelativeDateSchema = (t: (key: string) => string) => z.object({
|
||||
});
|
||||
|
||||
const getDateSelectionSchema = (t: (key: string) => string) => z.union([
|
||||
z.object({ fixed: z.string({ message: t('accounts.selectDate') }) },),
|
||||
z.object({ fixed: z.string({ message: t('accounts.selectDate') }) }),
|
||||
z.object({ relative: getRelativeDateSchema(t) }),
|
||||
z.undefined(),
|
||||
]);
|
||||
@@ -107,8 +106,13 @@ export type Account = {
|
||||
value?: number;
|
||||
};
|
||||
};
|
||||
date_before?: {
|
||||
unit?: 'Days' | 'Months' | 'Years';
|
||||
value?: number;
|
||||
};
|
||||
folder_limit?: number;
|
||||
sync_interval_min: number;
|
||||
sync_batch_size: number;
|
||||
};
|
||||
|
||||
const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
|
||||
@@ -119,12 +123,18 @@ const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
|
||||
enabled: z.boolean(),
|
||||
use_dangerous: z.boolean(),
|
||||
date_since: getDateSelectionSchema(t).optional(),
|
||||
date_before: getRelativeDateSchema(t).optional(),
|
||||
folder_limit: z
|
||||
.number({ invalid_type_error: t('validation.folderLimitMustBeNumber') })
|
||||
.int()
|
||||
.min(100, { message: t('validation.folderLimitMustBeAtLeast100') })
|
||||
.optional(),
|
||||
sync_interval_min: z.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') }).int().min(10, { message: t('validation.incrementalSyncMustBeAtLeast10') }),
|
||||
sync_batch_size: z
|
||||
.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') })
|
||||
.int()
|
||||
.min(30, { message: t('validation.incrementalSyncMustBeAtLeast10') })
|
||||
.max(200, { message: t('validation.incrementalSyncMustBeAtLeast10') }),
|
||||
});
|
||||
|
||||
type Step = {
|
||||
@@ -133,14 +143,12 @@ type Step = {
|
||||
fields: (keyof Account)[];
|
||||
};
|
||||
|
||||
export type Steps = [
|
||||
...Step[]
|
||||
];
|
||||
export type Steps = [...Step[]];
|
||||
|
||||
const getSteps = (t: (key: string) => string): Steps => [
|
||||
{ id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email"] },
|
||||
{ id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "name"] },
|
||||
{ id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "folder_limit", "sync_interval_min"] },
|
||||
{ id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "folder_limit", "sync_interval_min", "sync_batch_size"] },
|
||||
{ id: "step-4", name: t('accounts.steps.summary'), fields: [] },
|
||||
];
|
||||
|
||||
@@ -168,8 +176,10 @@ const defaultValues: Account = {
|
||||
enabled: true,
|
||||
use_dangerous: false,
|
||||
date_since: undefined,
|
||||
date_before: undefined,
|
||||
folder_limit: undefined,
|
||||
sync_interval_min: 10,
|
||||
sync_batch_size: 50,
|
||||
};
|
||||
|
||||
const emptyImap: ImapConfig = {
|
||||
@@ -194,8 +204,10 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => {
|
||||
enabled: currentRow.enabled,
|
||||
use_dangerous: currentRow.use_dangerous,
|
||||
date_since: currentRow.date_since ?? undefined,
|
||||
date_before: currentRow.date_before ?? undefined,
|
||||
folder_limit: currentRow.folder_limit ?? undefined,
|
||||
sync_interval_min: currentRow.sync_interval_min ?? 10,
|
||||
sync_batch_size: currentRow.sync_batch_size ?? 50,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -272,8 +284,10 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
enabled: data.enabled,
|
||||
use_dangerous: data.use_dangerous,
|
||||
date_since: data.date_since,
|
||||
date_before: data.date_before,
|
||||
folder_limit: data.folder_limit,
|
||||
sync_interval_min: data.sync_interval_min,
|
||||
sync_batch_size: data.sync_batch_size,
|
||||
};
|
||||
if (isEdit) {
|
||||
updateMutation.mutate(commonData);
|
||||
@@ -330,61 +344,67 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(state) => {
|
||||
form.reset();
|
||||
setCurrentStep(1);
|
||||
if (!state) {
|
||||
form.reset();
|
||||
setCurrentStep(1);
|
||||
}
|
||||
onOpenChange(state);
|
||||
}}
|
||||
>
|
||||
<DialogContent className='max-w-5xl'>
|
||||
<DialogHeader className='text-left mb-4'>
|
||||
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
|
||||
{t('accounts.clickSaveWhenDone')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[38rem] w-full pr-4 -mr-4 py-1">
|
||||
<>
|
||||
<div className="flex my-5 space-x-4 md:hidden">
|
||||
{steps.map((step, index) => (
|
||||
<DialogContent className="max-w-[95vw] md:max-w-5xl w-full p-0 overflow-hidden flex flex-col h-[90vh]">
|
||||
<div className="p-6 pb-2 flex-shrink-0">
|
||||
<DialogHeader className="text-left">
|
||||
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
|
||||
{t('accounts.clickSaveWhenDone')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden border-y">
|
||||
<div className="md:hidden flex px-6 py-2 space-x-2 overflow-x-auto border-b flex-shrink-0 bg-background/50">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex flex-col items-center flex-shrink-0 min-w-[70px]">
|
||||
<Button
|
||||
key={step.id}
|
||||
className={`size-9 rounded-full border font-bold ${currentStep === index + 1 ? "bg-primary text-white" : "bg-gray-200 text-black"
|
||||
}`}
|
||||
variant={currentStep === index + 1 ? "default" : "secondary"}
|
||||
className="size-8 rounded-full font-bold p-0"
|
||||
disabled={currentStep === index + 1}
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full max-w-full p-4">
|
||||
<div className="flex md:h-min rounded-xl md:rounded-2xl p-4">
|
||||
<div className="hidden md:block w-[260px] flex-shrink-0 rounded-xl p-5 pt-7 fixed">
|
||||
{steps.map((step, index) => (
|
||||
<div className="my-3 ml-2 flex items-center" key={step.id}>
|
||||
<Button
|
||||
className={`size-8 border rounded-full text-sm font-bold ${currentStep === index + 1 ? "bg-primary text-white" : "bg-gray-200 text-black"
|
||||
}`}
|
||||
disabled={currentStep === index + 1}
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<div className="flex flex-col items-baseline uppercase ml-5">
|
||||
<span className="text-xs">{t('accounts.step', { index: index + 1 })}</span>
|
||||
<span className="font-bold text-sm tracking-wider">{step.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[10px] mt-1 text-muted-foreground line-clamp-1">{step.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block w-[240px] flex-shrink-0 px-8 py-4 border-r overflow-y-auto">
|
||||
{steps.map((step, index) => (
|
||||
<div className="mb-8 flex items-center" key={step.id}>
|
||||
<Button
|
||||
variant={currentStep === index + 1 ? "default" : "secondary"}
|
||||
className="size-9 rounded-full text-sm font-bold"
|
||||
disabled={currentStep === index + 1}
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<div className="flex flex-col items-baseline uppercase ml-4">
|
||||
<span className="text-[10px] text-muted-foreground">{t('accounts.step', { index: index + 1 })}</span>
|
||||
<span className={cn("font-bold text-sm tracking-wider", currentStep === index + 1 ? "text-foreground" : "text-muted-foreground")}>
|
||||
{step.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-6 md:p-10 lg:p-14">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="account-register-form"
|
||||
className="flex-grow flex flex-col px-4 md:px-8 lg:px-12 ml-[240px]"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<form id="account-register-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{currentStep === 1 && <Step1 isEdit={isEdit} />}
|
||||
{currentStep === 2 && <Step2 isEdit={isEdit} />}
|
||||
{currentStep === 3 && <Step3 />}
|
||||
@@ -392,14 +412,16 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</ScrollArea>
|
||||
<DialogFooter className="flex flex-wrap gap-2">
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 md:p-6 bg-background flex flex-row sm:justify-end gap-2 flex-shrink-0">
|
||||
{currentStep > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-grow sm:flex-grow-0 shadow-none text-nowrap text-sm"
|
||||
variant="outline"
|
||||
className="flex-1 sm:flex-none"
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
>
|
||||
{t('accounts.goBack')}
|
||||
@@ -408,7 +430,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
{currentStep < LAST_STEP && (
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-grow sm:flex-grow-0 rounded-md md:rounded-lg px-6 text-sm"
|
||||
className="flex-1 sm:flex-none px-8"
|
||||
onClick={handleContinue}
|
||||
>
|
||||
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
|
||||
@@ -418,7 +440,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
||||
<Button
|
||||
type="submit"
|
||||
form="account-register-form"
|
||||
className="flex-grow sm:flex-grow-0 rounded-md text-sm px-7 md:rounded-lg"
|
||||
className="flex-1 sm:flex-none px-10"
|
||||
>
|
||||
{isEdit ? t('accounts.saveChanges') : t('accounts.submit')}
|
||||
</Button>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import LongText from '@/components/long-text'
|
||||
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { DataTableColumnHeader } from './data-table-column-header'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
import { format } from 'date-fns'
|
||||
@@ -28,6 +26,7 @@ import { OAuth2Action } from './oauth2-action'
|
||||
import { RunningStateCellAction } from './running-state-action'
|
||||
import { EnableAction } from './enable-action'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
export function useColumns(): ColumnDef<AccountModel>[] {
|
||||
const { t } = useTranslation()
|
||||
@@ -112,7 +111,7 @@ export function useColumns(): ColumnDef<AccountModel>[] {
|
||||
{
|
||||
accessorKey: 'created_by',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Owner" className="justify-center" />
|
||||
<DataTableColumnHeader column={column} title={t('accounts.owner')} className="justify-center" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { created_user_name, created_user_email } = row.original;
|
||||
|
||||
@@ -30,10 +30,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useAccountContext } from '../context'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { Mailbox, MessageSquareMore } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
interface DataTableRowActionsProps {
|
||||
row: Row<AccountModel>
|
||||
@@ -113,7 +113,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
setOpen('access-assign')
|
||||
}}
|
||||
>
|
||||
<span>Access Control</span>
|
||||
<span>{t('accounts.accessControl')}</span>
|
||||
<DropdownMenuShortcut>
|
||||
<IconShieldLock size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
|
||||
@@ -24,11 +24,10 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { AxiosError } from 'axios'
|
||||
import { remove_account } from '@/api/account/api'
|
||||
import { AccountModel, remove_account } from '@/api/account/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
|
||||
|
||||
import { Row } from '@tanstack/react-table'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useState } from 'react'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { update_account } from '@/api/account/api'
|
||||
import { AccountModel, update_account } from '@/api/account/api'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -28,12 +28,11 @@ import { AxiosError } from 'axios';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { create_account, update_account } from '@/api/account/api';
|
||||
import { AccountModel, create_account, update_account } from '@/api/account/api';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { AccountModel } from '../data/schema';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
import { Row } from '@tanstack/react-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAccountContext } from '../context'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
interface DataTableRowActionsProps {
|
||||
row: Row<AccountModel>
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { get_oauth2_tokens } from '@/api/oauth2/api'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -44,6 +43,7 @@ import { ToastAction } from '@/components/ui/toast'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { dateFnsLocaleMap } from '@/lib/utils'
|
||||
import { enUS } from 'date-fns/locale'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
interface Props {
|
||||
currentRow: AccountModel
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
|
||||
import { Row } from '@tanstack/react-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AccountModel } from '../data/schema';
|
||||
import { useAccountContext } from '../context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCurrentUser } from '@/hooks/use-current-user';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { ToastAction } from '@/components/ui/toast';
|
||||
import { AccountModel } from '@/api/account/api';
|
||||
|
||||
interface Props {
|
||||
row: Row<AccountModel>
|
||||
|
||||
@@ -25,9 +25,8 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { account_state } from '@/api/account/api'
|
||||
import { account_state, AccountModel } from '@/api/account/api'
|
||||
import { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
@@ -39,189 +39,206 @@ import { Button } from "@/components/ui/button";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { cn, dateFnsLocaleMap } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { enUS } from "date-fns/locale";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
|
||||
type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative';
|
||||
|
||||
export default function Step3() {
|
||||
const { t } = useTranslation();
|
||||
const { control, getValues, setValue } = useFormContext<Account>();
|
||||
const current = getValues();
|
||||
const [rangeType, setRangeType] = useState<'none' | 'fixed' | 'relative'>(
|
||||
current.date_since ? (current.date_since.fixed ? 'fixed' : 'relative') : 'none'
|
||||
);
|
||||
|
||||
const [syncMode, setSyncMode] = useState<SyncMode>(() => {
|
||||
if (current.date_before) return 'before_relative';
|
||||
if (current.date_since?.fixed) return 'since_fixed';
|
||||
if (current.date_since?.relative) return 'since_relative';
|
||||
return 'all';
|
||||
});
|
||||
|
||||
|
||||
const handleModeChange = (mode: SyncMode) => {
|
||||
setSyncMode(mode);
|
||||
|
||||
setValue("date_since", undefined);
|
||||
setValue("date_before", undefined);
|
||||
|
||||
if (mode === 'since_fixed') {
|
||||
setValue("date_since.fixed", undefined);
|
||||
} else if (mode === 'since_relative') {
|
||||
setValue("date_since.relative", { value: 1, unit: 'Months' });
|
||||
} else if (mode === 'before_relative') {
|
||||
setValue("date_before", { value: 1, unit: 'Years' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<FormField
|
||||
control={control}
|
||||
name="sync_interval_min"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">
|
||||
{t('accounts.incrementalSync')}:
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('accounts.incrementalSyncPlaceholder')}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField
|
||||
control={control}
|
||||
name="sync_interval_min"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.incrementalSync')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t('accounts.incrementalSyncDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name="sync_batch_size"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('accounts.syncBatchSize')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t('accounts.syncBatchSizeDescription')}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start gap-y-1">
|
||||
<FormLabel>{t('accounts.enabled')}:</FormLabel>
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow-sm">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
className="mt-2"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>{t('accounts.enabled')}</FormLabel>
|
||||
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormLabel className="flex items-center justify-between">{t('accounts.dateSince')}:</FormLabel>
|
||||
<RadioGroup
|
||||
defaultValue={rangeType}
|
||||
onValueChange={(value: 'fixed' | 'relative' | 'none') => {
|
||||
setRangeType(value);
|
||||
if (value === 'none') {
|
||||
setValue("date_since", undefined, { shouldValidate: true });
|
||||
}
|
||||
if (value === 'fixed') {
|
||||
setValue("date_since", { fixed: undefined }, { shouldValidate: true });
|
||||
}
|
||||
if (value === 'relative') {
|
||||
setValue("date_since", { relative: { value: undefined, unit: undefined } }, { shouldValidate: true });
|
||||
}
|
||||
}}
|
||||
className="flex flex-row space-x-4"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="none" />
|
||||
<FormLabel className="font-normal">{t('accounts.none')}</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="fixed" />
|
||||
<FormLabel className="font-normal">{t('accounts.fixed')}</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="relative" />
|
||||
<FormLabel className="font-normal">{t('accounts.relative')}</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
|
||||
<FormDescription>
|
||||
{t('accounts.syncStartDateDescription', {
|
||||
fixedPart: rangeType === 'fixed' ? t('accounts.syncAfterDate') : t('accounts.syncRecentData'),
|
||||
})}
|
||||
</FormDescription>
|
||||
<hr className="my-4" />
|
||||
<div className="space-y-4">
|
||||
<FormItem>
|
||||
<FormLabel className="text-base font-semibold">{t('accounts.syncScope', 'Sync Strategy')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('accounts.syncScopeDescription', 'Choose which emails should be indexed and archived.')}
|
||||
</FormDescription>
|
||||
<Select value={syncMode} onValueChange={(v) => handleModeChange(v as SyncMode)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('accounts.selectMode')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t('accounts.syncAll', 'Sync All Emails')}</SelectItem>
|
||||
<SelectItem value="since_fixed">{t('accounts.sinceFixed', 'Since Specific Date')}</SelectItem>
|
||||
<SelectItem value="since_relative">{t('accounts.sinceRelative', 'Keep Recent Emails')}</SelectItem>
|
||||
<SelectItem value="before_relative">{t('accounts.beforeRelative', 'Archive Old Emails Only')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<div className="pl-2 border-l-2 border-primary/20 space-y-4 pt-2">
|
||||
{syncMode === 'since_fixed' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.fixed"
|
||||
render={({ field }) => {
|
||||
const currentLang = i18n.language.toLowerCase().replace('_', '-');
|
||||
const dateLocale = dateFnsLocaleMap[currentLang] || enUS;
|
||||
return <FormItem className="flex flex-col">
|
||||
<FormLabel>{t('accounts.selectDate')}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-[440px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
||||
>
|
||||
{field.value ? format(new Date(field.value), "PPP", { locale: dateLocale }) : <span>{t('accounts.selectDate')}</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toLocaleDateString('en-CA'))}
|
||||
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
||||
locale={dateLocale}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>;
|
||||
|
||||
{rangeType === 'fixed' && (
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.fixed"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal text-brand-marine-blue",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? format(field.value, "PPP") : <span>{t('accounts.selectDate')}</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(new Date(field.value).setHours(0, 0, 0, 0)) : undefined}
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
const formattedDate = value.toLocaleDateString('en-CA');
|
||||
field.onChange(formattedDate);
|
||||
} else {
|
||||
field.onChange(null);
|
||||
}
|
||||
}}
|
||||
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rangeType === 'relative' && (
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.relative.value"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="e.g. 1" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<FormField
|
||||
control={control}
|
||||
name="date_since.relative.unit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
{(syncMode === 'since_relative' || syncMode === 'before_relative') && (
|
||||
<div className="flex flex-row items-end gap-4 animate-in fade-in slide-in-from-left-2">
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.value" : "date_before.value"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1 max-w-[150px]">
|
||||
<FormLabel>{t('accounts.duration', 'Duration')}</FormLabel>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectUnit')} />
|
||||
</SelectTrigger>
|
||||
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
|
||||
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
|
||||
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={syncMode === 'since_relative' ? "date_since.relative.unit" : "date_before.unit"}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-[180px]">
|
||||
<FormLabel>{t('accounts.unit', 'Unit')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('accounts.selectUnit')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
|
||||
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
|
||||
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name="folder_limit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center justify-between">{t('accounts.folderLimit')}:</FormLabel>
|
||||
<FormLabel>{t('accounts.folderLimit')}</FormLabel>
|
||||
<FormDescription>{t('accounts.folderLimitDescription')}</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -237,4 +254,4 @@ export default function Step3() {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,28 @@ export default function Step4() {
|
||||
const { getValues } = useFormContext<Account>();
|
||||
const summaryData = getValues();
|
||||
|
||||
|
||||
const sinceText = (() => {
|
||||
if (summaryData.date_since?.fixed) {
|
||||
return summaryData.date_since.fixed;
|
||||
}
|
||||
|
||||
if (summaryData.date_since?.relative?.value) {
|
||||
return `${t('accounts.sinceRelativeValue', {
|
||||
value: summaryData.date_since!.relative!.value,
|
||||
unit: t(`accounts.${summaryData.date_since!.relative!.unit!.toLowerCase()}`)
|
||||
})}`;
|
||||
}
|
||||
|
||||
return t('accounts.syncAll');
|
||||
})();
|
||||
|
||||
const hasSince = !!summaryData.date_since;
|
||||
const hasBefore = !!summaryData.date_before?.value;
|
||||
|
||||
return (
|
||||
<div className="p-5 rounded-xl">
|
||||
<Accordion type="multiple" defaultValue={['email', 'name', 'imap', 'date_since', 'folder_limit', 'sync_interval']}>
|
||||
<Accordion type="multiple" defaultValue={['email', 'name', 'imap', 'date_since', 'folder_limit', 'sync_interval', 'sync_scope', 'sync_batch_size']}>
|
||||
<AccordionItem key="email" value="email">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.email')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.email}</AccordionContent>
|
||||
@@ -82,17 +101,44 @@ export default function Step4() {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="date_since" value="date_since">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.dateSelection')}:</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{summaryData.date_since?.fixed
|
||||
? t('accounts.since') + ' ' + summaryData.date_since.fixed
|
||||
: summaryData.date_since?.relative && summaryData.date_since.relative.value && summaryData.date_since.relative.unit
|
||||
? t('accounts.recent') + ' ' + summaryData.date_since.relative.value + ' ' + summaryData.date_since.relative.unit
|
||||
: t('accounts.notAvailable')}
|
||||
<AccordionItem key="sync_scope" value="sync_scope">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">
|
||||
{t('accounts.syncScope')}:
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="space-y-3">
|
||||
{hasSince && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.sinceFixed')}:
|
||||
</span>
|
||||
<span className="text-sm">{sinceText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasBefore && (
|
||||
<div className="flex flex-col border-t pt-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('accounts.beforeRelative')}:
|
||||
</span>
|
||||
<span className="text-sm">
|
||||
{t('accounts.beforeRelativeValue', {
|
||||
value: summaryData.date_before!.value,
|
||||
unit: t(`accounts.${summaryData.date_before!.unit!.toLowerCase()}`)
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSince && !hasBefore && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('accounts.syncAll')}
|
||||
</span>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
<AccordionItem key="folder_limit" value="folder_limit">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.folderLimit')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.folder_limit ?? t('accounts.notAvailable')}</AccordionContent>
|
||||
@@ -102,6 +148,11 @@ export default function Step4() {
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.incrementalSync')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.sync_interval_min} {t('accounts.minutes')}</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem key="sync_batch_size" value="sync_batch_size">
|
||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.syncBatchSize')}:</AccordionTrigger>
|
||||
<AccordionContent>{summaryData.sync_batch_size}</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,12 +29,11 @@ import { Button } from '@/components/ui/button'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Loader2, CheckSquare, Square } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
|
||||
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { update_account } from '@/api/account/api'
|
||||
import { AccountModel, update_account } from '@/api/account/api'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
|
||||
@@ -41,10 +41,10 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { AccountModel } from '../data/schema'
|
||||
import { DataTablePagination } from './data-table-pagination'
|
||||
import { DataTableToolbar } from './data-table-toolbar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AccountModel } from '@/api/account/api'
|
||||
|
||||
declare module '@tanstack/react-table' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import { AccountModel } from '@/api/account/api';
|
||||
import React from 'react'
|
||||
import { AccountModel } from '../data/schema'
|
||||
|
||||
export type AccountDialogType =
|
||||
| 'add-imap'
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2025 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/>.
|
||||
|
||||
|
||||
type Encryption = 'Ssl' | 'StartTls' | 'None';
|
||||
type AuthType = 'Password' | 'OAuth2';
|
||||
type Unit = 'Days' | 'Months' | 'Years';
|
||||
type AccountType = 'IMAP' | 'NoSync';
|
||||
// Interface definitions
|
||||
interface AuthConfig {
|
||||
auth_type: AuthType;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface ImapConfig {
|
||||
host: string;
|
||||
port: number; // integer, 0-65535
|
||||
encryption: Encryption;
|
||||
auth: AuthConfig;
|
||||
use_proxy?: number;
|
||||
}
|
||||
|
||||
interface RelativeDate {
|
||||
unit: Unit;
|
||||
value: number; // integer, minimum 1
|
||||
}
|
||||
|
||||
interface DateSelection {
|
||||
fixed?: string; // format: "YYYY-MM-DD"
|
||||
relative?: RelativeDate;
|
||||
}
|
||||
|
||||
export interface AccountModel {
|
||||
id: number;
|
||||
account_type: AccountType;
|
||||
imap?: ImapConfig;
|
||||
enabled: boolean;
|
||||
name?: string,
|
||||
email: string;
|
||||
capabilities?: string[];
|
||||
date_since?: DateSelection;
|
||||
folder_limit?: number,
|
||||
sync_folders: string[];
|
||||
sync_interval_min?: number;
|
||||
created_by: number;
|
||||
created_user_name: string;
|
||||
created_user_email: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
use_proxy?: number
|
||||
use_dangerous: boolean
|
||||
}
|
||||
@@ -30,9 +30,8 @@ import AccountProvider, {
|
||||
} from './context'
|
||||
import { MoreVertical, Plus } from 'lucide-react'
|
||||
import Logo from '@/assets/logo.svg'
|
||||
import { AccountModel } from './data/schema'
|
||||
import { AccountDetailDrawer } from './components/account-detail'
|
||||
import { list_accounts } from '@/api/account/api'
|
||||
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'
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "إصدار النظام"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "مزامنة رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت",
|
||||
"sinceRelativeValue": "مزامنة رسائل البريد الإلكتروني لآخر {{value}} {{unit}}",
|
||||
"syncBatchSize": "حجم دفعة المزامنة",
|
||||
"syncBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP",
|
||||
"incrementalSyncDescription": "عدد مرات إجراء مزامنة البريد الإلكتروني المتزايدة (بالدقائق)",
|
||||
"syncScope": "استراتيجية المزامنة",
|
||||
"syncScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وأرشفتها.",
|
||||
"selectMode": "حدد وضع التصفية",
|
||||
"syncAll": "مزامنة جميع رسائل البريد الإلكتروني",
|
||||
"sinceFixed": "منذ تاريخ محدد",
|
||||
"sinceRelative": "مزامنة رسائل البريد الإلكتروني الحديثة فقط",
|
||||
"beforeRelative": "أرشفة رسائل البريد الإلكتروني القديمة فقط",
|
||||
"duration": "المدة",
|
||||
"unit": "الوحدة",
|
||||
"accessControl": "التحكم في الوصول",
|
||||
"owner": "المنشئ",
|
||||
"access_control": {
|
||||
"title": "تخصيص الوصول للحساب",
|
||||
"description": "تعيين الأدوار والمستخدمين المفوضين لـ {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Systemversion"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synkroniser e-mails fra før {{value}} {{unit}} siden",
|
||||
"sinceRelativeValue": "Synkroniser e-mails fra de seneste {{value}} {{unit}}",
|
||||
"syncBatchSize": "Synkroniseringsbatchstørrelse",
|
||||
"syncBatchSizeDescription": "Antal beskeder hentet per IMAP-forespørgsel",
|
||||
"incrementalSyncDescription": "Hvor ofte inkrementel e-mail-synkronisering udføres (i minutter)",
|
||||
"syncScope": "Synkroniseringsstrategi",
|
||||
"syncScopeDescription": "Vælg hvilke e-mails der skal indekseres og arkiveres.",
|
||||
"selectMode": "Vælg filtertilstand",
|
||||
"syncAll": "Synkroniser alle e-mails",
|
||||
"sinceFixed": "Siden en bestemt dato",
|
||||
"sinceRelative": "Synkroniser kun nylige e-mails",
|
||||
"beforeRelative": "Arkiver kun gamle e-mails",
|
||||
"duration": "Varighed",
|
||||
"unit": "Enhed",
|
||||
"accessControl": "Adgangskontrol",
|
||||
"owner": "Oprettet af",
|
||||
"access_control": {
|
||||
"title": "Tildeling af kontoadgang",
|
||||
"description": "Tildel roller og autoriserede brugere til {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Systemversion"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "E-Mails synchronisieren, die älter als {{value}} {{unit}} sind",
|
||||
"sinceRelativeValue": "E-Mails der letzten {{value}} {{unit}} synchronisieren",
|
||||
"syncBatchSize": "Synchronisations-Batch-Größe",
|
||||
"syncBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten",
|
||||
"incrementalSyncDescription": "Häufigkeit der inkrementellen E-Mail-Synchronisierung (in Minuten)",
|
||||
"syncScope": "Synchronisationsstrategie",
|
||||
"syncScopeDescription": "Wählen Sie aus, welche E-Mails indiziert und archiviert werden sollen.",
|
||||
"selectMode": "Filtermodus auswählen",
|
||||
"syncAll": "Alle E-Mails synchronisieren",
|
||||
"sinceFixed": "Seit einem bestimmten Datum",
|
||||
"sinceRelative": "Nur aktuelle E-Mails synchronisieren",
|
||||
"beforeRelative": "Nur alte E-Mails archivieren",
|
||||
"duration": "Dauer",
|
||||
"unit": "Einheit",
|
||||
"accessControl": "Zugriffskontrolle",
|
||||
"owner": "Ersteller",
|
||||
"access_control": {
|
||||
"title": "Kontozugriffszuweisung",
|
||||
"description": "Rollen und autorisierte Benutzer für {{email}} zuweisen",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "System Version"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Sync emails before {{value}} {{unit}} ago",
|
||||
"sinceRelativeValue": "Sync emails from the last",
|
||||
"syncBatchSize": "Sync batch size",
|
||||
"syncBatchSizeDescription": "Number of messages fetched per IMAP request",
|
||||
"incrementalSyncDescription": "How often incremental email synchronization is performed (in minutes)",
|
||||
"syncScopeDescription": "Choose which emails should be indexed and archived.",
|
||||
"syncScope": "Sync Strategy",
|
||||
"selectMode": "Select filter mode",
|
||||
"syncAll": "Sync All Emails",
|
||||
"sinceFixed": "Since Specific Date",
|
||||
"sinceRelative": "Sync Recent Emails Only",
|
||||
"beforeRelative": "Archive Old Emails Only",
|
||||
"duration": "Duration",
|
||||
"unit": "Unit",
|
||||
"accessControl": "Access Control",
|
||||
"owner": "Creator",
|
||||
"access_control": {
|
||||
"title": "Account Access Assignment",
|
||||
"description": "Assign roles and authorized users to {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Versión del sistema"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Sincronizar correos de hace más de {{value}} {{unit}}",
|
||||
"sinceRelativeValue": "Sincronizar correos de los últimos {{value}} {{unit}}",
|
||||
"syncBatchSize": "Tamaño del lote de sincronización",
|
||||
"syncBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP",
|
||||
"incrementalSyncDescription": "Frecuencia de sincronización incremental (en minutos)",
|
||||
"syncScope": "Estrategia de sincronización",
|
||||
"syncScopeDescription": "Elija qué correos deben indexarse y archivarse.",
|
||||
"selectMode": "Seleccionar modo de filtro",
|
||||
"syncAll": "Sincronizar todos los correos",
|
||||
"sinceFixed": "Desde una fecha específica",
|
||||
"sinceRelative": "Sincronizar solo correos recientes",
|
||||
"beforeRelative": "Archivar solo correos antiguos",
|
||||
"duration": "Duración",
|
||||
"unit": "Unidad",
|
||||
"accessControl": "Control de acceso",
|
||||
"owner": "Creador",
|
||||
"access_control": {
|
||||
"title": "Asignación de Acceso a la Cuenta",
|
||||
"description": "Asignar roles y usuarios autorizados a {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Järjestelmäversio"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synkronoi sähköpostit, jotka ovat vanhempia kuin {{value}} {{unit}}",
|
||||
"sinceRelativeValue": "Synkronoi viimeisimmän {{value}} {{unit}} sähköpostit",
|
||||
"syncBatchSize": "Synkronoinnin eräkoko",
|
||||
"syncBatchSizeDescription": "Per IMAP-pyyntö noudettujen viestien määrä",
|
||||
"incrementalSyncDescription": "Kuinka usein inkrementaalinen sähköpostin synkronointi suoritetaan (minuutteina)",
|
||||
"syncScope": "Synkronointistrategia",
|
||||
"syncScopeDescription": "Valitse mitkä sähköpostit indeksoidaan ja arkistoidaan.",
|
||||
"selectMode": "Valitse suodatustila",
|
||||
"syncAll": "Synkronoi kaikki sähköpostit",
|
||||
"sinceFixed": "Tietystä päivämäärästä lähtien",
|
||||
"sinceRelative": "Synkronoi vain viimeisimmät sähköpostit",
|
||||
"beforeRelative": "Arkistoi vain vanhat sähköpostit",
|
||||
"duration": "Kesto",
|
||||
"unit": "Yksikkö",
|
||||
"accessControl": "Pääsynhallinta",
|
||||
"owner": "Luoja",
|
||||
"access_control": {
|
||||
"title": "Tilin pääsynhallinta",
|
||||
"description": "Määritä roolit ja valtuutetut käyttäjät kohteelle {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Version du système"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synchroniser les e-mails datant de plus de {{value}} {{unit}}",
|
||||
"sinceRelativeValue": "Synchroniser les e-mails des derniers {{value}} {{unit}}",
|
||||
"syncBatchSize": "Taille du lot de synchronisation",
|
||||
"syncBatchSizeDescription": "Nombre de messages récupérés par requête IMAP",
|
||||
"incrementalSyncDescription": "Fréquence de synchronisation incrémentielle (en minutes)",
|
||||
"syncScope": "Stratégie de synchronisation",
|
||||
"syncScopeDescription": "Choisissez les e-mails à indexer et à archiver.",
|
||||
"selectMode": "Sélectionner le mode de filtrage",
|
||||
"syncAll": "Synchroniser tous les e-mails",
|
||||
"sinceFixed": "Depuis une date spécifique",
|
||||
"sinceRelative": "Synchroniser uniquement les e-mails récents",
|
||||
"beforeRelative": "Archiver uniquement les anciens e-mails",
|
||||
"duration": "Durée",
|
||||
"unit": "Unité",
|
||||
"accessControl": "Contrôle d'accès",
|
||||
"owner": "Créateur",
|
||||
"access_control": {
|
||||
"title": "Attribution d'accès au compte",
|
||||
"description": "Attribuer des rôles et des utilisateurs autorisés à {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Versione del sistema"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Sincronizza le email antecedenti a {{value}} {{unit}} fa",
|
||||
"sinceRelativeValue": "Sincronizza le email degli ultimi {{value}} {{unit}}",
|
||||
"syncBatchSize": "Dimensione batch di sincronizzazione",
|
||||
"syncBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP",
|
||||
"incrementalSyncDescription": "Frequenza della sincronizzazione incrementale (in minuti)",
|
||||
"syncScope": "Strategia di sincronizzazione",
|
||||
"syncScopeDescription": "Scegli quali email indicizzare e archiviare.",
|
||||
"selectMode": "Seleziona modalità filtro",
|
||||
"syncAll": "Sincronizza tutte le email",
|
||||
"sinceFixed": "Da una data specifica",
|
||||
"sinceRelative": "Sincronizza solo email recenti",
|
||||
"beforeRelative": "Archivia solo email vecchie",
|
||||
"duration": "Durata",
|
||||
"unit": "Unità",
|
||||
"accessControl": "Controllo accessi",
|
||||
"owner": "Creatore",
|
||||
"access_control": {
|
||||
"title": "Assegnazione Accesso Account",
|
||||
"description": "Assegna ruoli e utenti autorizzati a {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "システムバージョン"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "{{value}} {{unit}} 前より前のメールを同期",
|
||||
"sinceRelativeValue": "過去 {{value}} {{unit}} 分のメールを同期",
|
||||
"syncBatchSize": "同期バッチサイズ",
|
||||
"syncBatchSizeDescription": "1回のIMAPリクエストで取得するメッセージ数",
|
||||
"incrementalSyncDescription": "増分メール同期の実行頻度(分単位)",
|
||||
"syncScope": "同期戦略",
|
||||
"syncScopeDescription": "インデックスを作成し、アーカイブするメールを選択します。",
|
||||
"selectMode": "フィルタモードを選択",
|
||||
"syncAll": "すべてのメールを同期",
|
||||
"sinceFixed": "指定した日付以降",
|
||||
"sinceRelative": "最近のメールのみ同期",
|
||||
"beforeRelative": "古いメールのみアーカイブ",
|
||||
"duration": "期間",
|
||||
"unit": "単位",
|
||||
"accessControl": "アクセス制御",
|
||||
"owner": "作成者",
|
||||
"access_control": {
|
||||
"title": "アカウントアクセス割り当て",
|
||||
"description": "{{email}} にロールと権限ユーザーを割り当てます",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "시스템 버전"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "{{value}} {{unit}} 전 이전 이메일 동기화",
|
||||
"sinceRelativeValue": "지난 {{value}} {{unit}} 동안의 이메일 동기화",
|
||||
"syncBatchSize": "동기화 배치 크기",
|
||||
"syncBatchSizeDescription": "IMAP 요청당 가져올 메시지 수",
|
||||
"incrementalSyncDescription": "증분 이메일 동기화 수행 빈도 (분 단위)",
|
||||
"syncScope": "동기화 전략",
|
||||
"syncScopeDescription": "인덱싱 및 아카이빙할 이메일을 선택하십시오.",
|
||||
"selectMode": "필터 모드 선택",
|
||||
"syncAll": "모든 이메일 동기화",
|
||||
"sinceFixed": "특정 날짜 이후",
|
||||
"sinceRelative": "최신 이메일만 동기화",
|
||||
"beforeRelative": "오래된 이메일만 아카이브",
|
||||
"duration": "기간",
|
||||
"unit": "단위",
|
||||
"accessControl": "액세스 제어",
|
||||
"owner": "생성자",
|
||||
"access_control": {
|
||||
"title": "계정 액세스 할당",
|
||||
"description": "{{email}}에 역할 및 권한 사용자를 할당합니다",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Systeemversie"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synchroniseer e-mails van vóór {{value}} {{unit}} geleden",
|
||||
"sinceRelativeValue": "Synchroniseer e-mails van de afgelopen {{value}} {{unit}}",
|
||||
"syncBatchSize": "Batchgrootte synchronisatie",
|
||||
"syncBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek",
|
||||
"incrementalSyncDescription": "Frequentie van incrementele synchronisatie (in minuten)",
|
||||
"syncScope": "Synchronisatiestrategie",
|
||||
"syncScopeDescription": "Kies welke e-mails geïndexeerd en gearchiveerd moeten worden.",
|
||||
"selectMode": "Filtermodus selecteren",
|
||||
"syncAll": "Alle e-mails synchroniseren",
|
||||
"sinceFixed": "Sinds een specifieke datum",
|
||||
"sinceRelative": "Alleen recente e-mails synchroniseren",
|
||||
"beforeRelative": "Alleen oude e-mails archiveren",
|
||||
"duration": "Duur",
|
||||
"unit": "Eenheid",
|
||||
"accessControl": "Toegangsbeheer",
|
||||
"owner": "Maker",
|
||||
"access_control": {
|
||||
"title": "Toewijzing accounttoegang",
|
||||
"description": "Rollen en geautoriseerde gebruikers toewijzen aan {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Systemversjon"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synkroniser e-poster fra før {{value}} {{unit}} siden",
|
||||
"sinceRelativeValue": "Synkroniser e-poster fra de siste {{value}} {{unit}}",
|
||||
"syncBatchSize": "Synkroniserings-batchstørrelse",
|
||||
"syncBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel",
|
||||
"incrementalSyncDescription": "Hvor ofte inkrementell e-post-synkronisering utføres (i minutter)",
|
||||
"syncScope": "Synkroniseringsstrategi",
|
||||
"syncScopeDescription": "Velg hvilke e-poster som skal indekseres og arkiveres.",
|
||||
"selectMode": "Velg filtermodus",
|
||||
"syncAll": "Synkroniser alle e-poster",
|
||||
"sinceFixed": "Siden spesifikk dato",
|
||||
"sinceRelative": "Synkroniser kun nylige e-poster",
|
||||
"beforeRelative": "Arkiver kun gamle e-poster",
|
||||
"duration": "Varighet",
|
||||
"unit": "Enhet",
|
||||
"accessControl": "Tilgangskontroll",
|
||||
"owner": "Opprettet av",
|
||||
"access_control": {
|
||||
"title": "Tildeling av kontotilgang",
|
||||
"description": "Tildel roller og autoriserte brukere til {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Wersja systemu"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synchronizuj wiadomości sprzed {{value}} {{unit}}",
|
||||
"sinceRelativeValue": "Synchronizuj wiadomości z ostatnich {{value}} {{unit}}",
|
||||
"syncBatchSize": "Rozmiar partii synchronizacji",
|
||||
"syncBatchSizeDescription": "Liczba wiadomości pobieranych w jednym żądaniu IMAP",
|
||||
"incrementalSyncDescription": "Częstotliwość wykonywania przyrostowej synchronizacji e-mail (w minutach)",
|
||||
"syncScope": "Strategia synchronizacji",
|
||||
"syncScopeDescription": "Wybierz wiadomości e-mail, które mają być indeksowane i archiwizowane.",
|
||||
"selectMode": "Wybierz tryb filtrowania",
|
||||
"syncAll": "Synchronizuj wszystkie wiadomości",
|
||||
"sinceFixed": "Od określonej daty",
|
||||
"sinceRelative": "Synchronizuj tylko ostatnie wiadomości",
|
||||
"beforeRelative": "Archiwizuj tylko stare wiadomości",
|
||||
"duration": "Czas trwania",
|
||||
"unit": "Jednostka",
|
||||
"accessControl": "Kontrola dostępu",
|
||||
"owner": "Twórca",
|
||||
"access_control": {
|
||||
"title": "Przypisywanie dostępu do konta",
|
||||
"description": "Przypisz role i uprawnionych użytkowników dla {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Versão do sistema"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Sincronizar e-mails de antes de {{value}} {{unit}} atrás",
|
||||
"sinceRelativeValue": "Sincronizar e-mails dos últimos {{value}} {{unit}}",
|
||||
"syncBatchSize": "Tamanho do lote de sincronização",
|
||||
"syncBatchSizeDescription": "Número de mensagens obtidas por solicitação IMAP",
|
||||
"incrementalSyncDescription": "Frequência da sincronização incremental (em minutos)",
|
||||
"syncScope": "Estratégia de sincronização",
|
||||
"syncScopeDescription": "Escolha quais e-mails devem ser indexados e arquivados.",
|
||||
"selectMode": "Selecionar modo de filtro",
|
||||
"syncAll": "Sincronizar todos os e-mails",
|
||||
"sinceFixed": "Desde uma data específica",
|
||||
"sinceRelative": "Sincronizar apenas e-mails recentes",
|
||||
"beforeRelative": "Arquivar apenas e-mails antigos",
|
||||
"duration": "Duração",
|
||||
"unit": "Unidade",
|
||||
"accessControl": "Controle de acesso",
|
||||
"owner": "Criador",
|
||||
"access_control": {
|
||||
"title": "Atribuição de Acesso à Conta",
|
||||
"description": "Atribuir funções e usuários autorizados a {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Версия системы"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Синхронизировать письма старее, чем {{value}} {{unit}} назад",
|
||||
"sinceRelativeValue": "Синхронизировать письма за последние {{value}} {{unit}}",
|
||||
"syncBatchSize": "Размер пакета синхронизации",
|
||||
"syncBatchSizeDescription": "Количество сообщений, получаемых за один запрос IMAP",
|
||||
"incrementalSyncDescription": "Частота инкрементной синхронизации почты (в минутах)",
|
||||
"syncScope": "Стратегия синхронизации",
|
||||
"syncScopeDescription": "Выберите письма для индексации и архивации.",
|
||||
"selectMode": "Выберите режим фильтрации",
|
||||
"syncAll": "Синхронизировать все письма",
|
||||
"sinceFixed": "С определенной даты",
|
||||
"sinceRelative": "Синхронизировать только новые письма",
|
||||
"beforeRelative": "Архивировать только старые письма",
|
||||
"duration": "Продолжительность",
|
||||
"unit": "Единица",
|
||||
"accessControl": "Контроль доступа",
|
||||
"owner": "Создатель",
|
||||
"access_control": {
|
||||
"title": "Назначение доступа к аккаунту",
|
||||
"description": "Назначить роли и авторизованных пользователей для {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "Systemversion"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "Synkronisera mejl från före {{value}} {{unit}} sedan",
|
||||
"sinceRelativeValue": "Synkronisera mejl från de senaste {{value}} {{unit}}",
|
||||
"syncBatchSize": "Batchstorlek för synk",
|
||||
"syncBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-förfrågan",
|
||||
"incrementalSyncDescription": "Hur ofta inkrementell e-post-synkronisering utförs (i minuter)",
|
||||
"syncScope": "Synkstrategi",
|
||||
"syncScopeDescription": "Välj vilka e-postmeddelanden som ska indexeras och arkiveras.",
|
||||
"selectMode": "Välj filterläge",
|
||||
"syncAll": "Synkronisera alla mejl",
|
||||
"sinceFixed": "Sedan ett specifikt datum",
|
||||
"sinceRelative": "Synka endast nyligen inkomna mejl",
|
||||
"beforeRelative": "Arkivera endast gamla mejl",
|
||||
"duration": "Varaktighet",
|
||||
"unit": "Enhet",
|
||||
"accessControl": "Åtkomstkontroll",
|
||||
"owner": "Skapad av",
|
||||
"access_control": {
|
||||
"title": "Tilldelning av kontotillgång",
|
||||
"description": "Tilldela roller och auktoriserade användare till {{email}}",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "系統版本"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的郵件",
|
||||
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 內的郵件",
|
||||
"syncBatchSize": "批次同步數量",
|
||||
"syncBatchSizeDescription": "每次 IMAP 請求獲取的郵件數量",
|
||||
"incrementalSyncDescription": "執行增量郵件同步的頻率(分鐘)",
|
||||
"syncScope": "同步策略",
|
||||
"syncScopeDescription": "選擇哪些郵件需要被索引和歸檔。",
|
||||
"selectMode": "選擇過濾模式",
|
||||
"syncAll": "同步所有郵件",
|
||||
"sinceFixed": "從特定日期開始 (至今)",
|
||||
"sinceRelative": "僅同步最近的郵件",
|
||||
"beforeRelative": "僅封存舊郵件",
|
||||
"duration": "時長",
|
||||
"unit": "單位",
|
||||
"accessControl": "訪問控制",
|
||||
"owner": "建立者",
|
||||
"access_control": {
|
||||
"title": "帳戶訪問分配",
|
||||
"description": "為 {{email}} 分配角色和授權用戶",
|
||||
|
||||
@@ -138,6 +138,22 @@
|
||||
"systemVersion": "系统版本"
|
||||
},
|
||||
"accounts": {
|
||||
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的邮件",
|
||||
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 内的邮件",
|
||||
"syncBatchSize": "批次同步数量",
|
||||
"syncBatchSizeDescription": "每次 IMAP 请求获取的邮件数量",
|
||||
"incrementalSyncDescription": "执行增量邮件同步的频率(分钟)",
|
||||
"syncScope": "同步策略",
|
||||
"syncScopeDescription": "选择哪些邮件需要被索引和归档。",
|
||||
"selectMode": "选择过滤模式",
|
||||
"syncAll": "同步所有邮件",
|
||||
"sinceFixed": "从特定日期开始 (至今)",
|
||||
"sinceRelative": "仅同步最近的邮件 (相对时间)",
|
||||
"beforeRelative": "仅同步旧邮件",
|
||||
"duration": "时长",
|
||||
"unit": "单位",
|
||||
"accessControl": "访问控制",
|
||||
"owner": "创建者",
|
||||
"access_control": {
|
||||
"title": "账户访问分配",
|
||||
"description": "为 {{email}} 分配角色和授权用户",
|
||||
|
||||
Reference in New Issue
Block a user