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:
@@ -27,7 +27,11 @@ use tracing::info;
|
|||||||
use crate::{
|
use crate::{
|
||||||
encrypt,
|
encrypt,
|
||||||
modules::{
|
modules::{
|
||||||
account::{entity::ImapConfig, since::DateSince, state::AccountRunningState},
|
account::{
|
||||||
|
entity::ImapConfig,
|
||||||
|
since::{DateSince, RelativeDate},
|
||||||
|
state::AccountRunningState,
|
||||||
|
},
|
||||||
cache::imap::mailbox::MailBox,
|
cache::imap::mailbox::MailBox,
|
||||||
database::{list_all_impl, with_transaction},
|
database::{list_all_impl, with_transaction},
|
||||||
error::BichonResult,
|
error::BichonResult,
|
||||||
@@ -136,10 +140,12 @@ pub struct AccountV3 {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub capabilities: Option<Vec<String>>,
|
pub capabilities: Option<Vec<String>>,
|
||||||
pub date_since: Option<DateSince>,
|
pub date_since: Option<DateSince>,
|
||||||
|
pub date_before: Option<RelativeDate>,
|
||||||
pub folder_limit: Option<u32>,
|
pub folder_limit: Option<u32>,
|
||||||
pub sync_folders: Option<Vec<String>>,
|
pub sync_folders: Option<Vec<String>>,
|
||||||
pub account_type: AccountType,
|
pub account_type: AccountType,
|
||||||
pub sync_interval_min: Option<i64>,
|
pub sync_interval_min: Option<i64>,
|
||||||
|
pub sync_batch_size: Option<u32>,
|
||||||
pub known_folders: Option<BTreeSet<String>>,
|
pub known_folders: Option<BTreeSet<String>>,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub updated_at: i64,
|
pub updated_at: i64,
|
||||||
@@ -174,6 +180,8 @@ impl AccountV3 {
|
|||||||
use_dangerous: request.use_dangerous,
|
use_dangerous: request.use_dangerous,
|
||||||
pgp_key: request.pgp_key,
|
pgp_key: request.pgp_key,
|
||||||
created_by: user_id,
|
created_by: user_id,
|
||||||
|
sync_batch_size: request.sync_batch_size,
|
||||||
|
date_before: request.date_before,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +412,12 @@ impl AccountV3 {
|
|||||||
|
|
||||||
if let Some(date_since) = request.date_since {
|
if let Some(date_since) = request.date_since {
|
||||||
new.date_since = Some(date_since);
|
new.date_since = Some(date_since);
|
||||||
|
new.date_before = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(date_before) = request.date_before {
|
||||||
|
new.date_before = Some(date_before);
|
||||||
|
new.date_since = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(folder_limit) = request.folder_limit {
|
if let Some(folder_limit) = request.folder_limit {
|
||||||
@@ -439,6 +453,11 @@ impl AccountV3 {
|
|||||||
if let Some(sync_interval_min) = &request.sync_interval_min {
|
if let Some(sync_interval_min) = &request.sync_interval_min {
|
||||||
new.sync_interval_min = Some(*sync_interval_min);
|
new.sync_interval_min = Some(*sync_interval_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(sync_batch_size) = &request.sync_batch_size {
|
||||||
|
new.sync_batch_size = Some(*sync_batch_size);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(use_proxy) = request.use_proxy {
|
if let Some(use_proxy) = request.use_proxy {
|
||||||
new.use_proxy = Some(use_proxy);
|
new.use_proxy = Some(use_proxy);
|
||||||
}
|
}
|
||||||
@@ -558,6 +577,8 @@ impl From<AccountV2> for AccountV3 {
|
|||||||
use_proxy: value.use_proxy,
|
use_proxy: value.use_proxy,
|
||||||
use_dangerous: value.use_dangerous,
|
use_dangerous: value.use_dangerous,
|
||||||
pgp_key: value.pgp_key,
|
pgp_key: value.pgp_key,
|
||||||
|
sync_batch_size: None,
|
||||||
|
date_before: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use crate::modules::account::entity::ImapConfig;
|
use crate::modules::account::entity::ImapConfig;
|
||||||
use crate::modules::account::migration::{AccountModel, AccountType};
|
use crate::modules::account::migration::{AccountModel, AccountType};
|
||||||
use crate::modules::account::since::DateSince;
|
use crate::modules::account::since::{DateSince, RelativeDate};
|
||||||
use crate::modules::error::code::ErrorCode;
|
use crate::modules::error::code::ErrorCode;
|
||||||
use crate::modules::error::BichonResult;
|
use crate::modules::error::BichonResult;
|
||||||
use crate::{raise_error, validate_email};
|
use crate::{raise_error, validate_email};
|
||||||
@@ -33,11 +33,14 @@ pub struct AccountCreateRequest {
|
|||||||
pub imap: Option<ImapConfig>,
|
pub imap: Option<ImapConfig>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub date_since: Option<DateSince>,
|
pub date_since: Option<DateSince>,
|
||||||
|
pub date_before: Option<RelativeDate>,
|
||||||
pub account_type: AccountType,
|
pub account_type: AccountType,
|
||||||
#[oai(validator(minimum(value = "100")))]
|
#[oai(validator(minimum(value = "100")))]
|
||||||
pub folder_limit: Option<u32>,
|
pub folder_limit: Option<u32>,
|
||||||
#[oai(validator(minimum(value = "10"), maximum(value = "480")))]
|
#[oai(validator(minimum(value = "10"), maximum(value = "480")))]
|
||||||
pub sync_interval_min: Option<i64>,
|
pub sync_interval_min: Option<i64>,
|
||||||
|
#[oai(validator(minimum(value = "30"), maximum(value = "200")))]
|
||||||
|
pub sync_batch_size: Option<u32>,
|
||||||
pub use_proxy: Option<u64>,
|
pub use_proxy: Option<u64>,
|
||||||
pub use_dangerous: bool,
|
pub use_dangerous: bool,
|
||||||
pub pgp_key: Option<String>,
|
pub pgp_key: Option<String>,
|
||||||
@@ -45,9 +48,22 @@ pub struct AccountCreateRequest {
|
|||||||
|
|
||||||
impl AccountCreateRequest {
|
impl AccountCreateRequest {
|
||||||
pub fn create_entity(self, user_id: u64) -> BichonResult<AccountModel> {
|
pub fn create_entity(self, user_id: u64) -> BichonResult<AccountModel> {
|
||||||
|
if self.date_before.is_some() && self.date_since.is_some() {
|
||||||
|
return Err(raise_error!(
|
||||||
|
"date_before and date_since are mutually exclusive; specify only one time boundary"
|
||||||
|
.into(),
|
||||||
|
ErrorCode::InvalidParameter
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(date_since) = self.date_since.as_ref() {
|
if let Some(date_since) = self.date_since.as_ref() {
|
||||||
date_since.validate()?;
|
date_since.validate()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(date_before) = self.date_before.as_ref() {
|
||||||
|
date_before.validate_date()?;
|
||||||
|
}
|
||||||
|
|
||||||
match self.account_type {
|
match self.account_type {
|
||||||
AccountType::IMAP => {
|
AccountType::IMAP => {
|
||||||
match &self.imap {
|
match &self.imap {
|
||||||
@@ -104,6 +120,7 @@ pub struct AccountUpdateRequest {
|
|||||||
/// - First-time sync optimization for large accounts
|
/// - First-time sync optimization for large accounts
|
||||||
/// - Reducing server load during resyncs
|
/// - Reducing server load during resyncs
|
||||||
pub date_since: Option<DateSince>,
|
pub date_since: Option<DateSince>,
|
||||||
|
pub date_before: Option<RelativeDate>,
|
||||||
/// Max emails to sync for this folder.
|
/// Max emails to sync for this folder.
|
||||||
/// If not set, sync all emails.
|
/// If not set, sync all emails.
|
||||||
/// otherwise sync up to `n` most recent emails (min 10).
|
/// otherwise sync up to `n` most recent emails (min 10).
|
||||||
@@ -126,6 +143,8 @@ pub struct AccountUpdateRequest {
|
|||||||
/// Incremental sync interval (seconds)
|
/// Incremental sync interval (seconds)
|
||||||
#[oai(validator(minimum(value = "10"), maximum(value = "480")))]
|
#[oai(validator(minimum(value = "10"), maximum(value = "480")))]
|
||||||
pub sync_interval_min: Option<i64>,
|
pub sync_interval_min: Option<i64>,
|
||||||
|
#[oai(validator(minimum(value = "30"), maximum(value = "200")))]
|
||||||
|
pub sync_batch_size: Option<u32>,
|
||||||
/// Optional proxy ID for establishing the connection to external APIs (e.g., Gmail, Outlook).
|
/// Optional proxy ID for establishing the connection to external APIs (e.g., Gmail, Outlook).
|
||||||
/// - If `None` or not provided, the client will connect directly to the API server.
|
/// - If `None` or not provided, the client will connect directly to the API server.
|
||||||
/// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests.
|
/// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests.
|
||||||
@@ -138,9 +157,22 @@ pub struct AccountUpdateRequest {
|
|||||||
|
|
||||||
impl AccountUpdateRequest {
|
impl AccountUpdateRequest {
|
||||||
pub fn validate_update_request(&self, account: &AccountModel) -> BichonResult<()> {
|
pub fn validate_update_request(&self, account: &AccountModel) -> BichonResult<()> {
|
||||||
|
if self.date_before.is_some() && self.date_since.is_some() {
|
||||||
|
return Err(raise_error!(
|
||||||
|
"date_before and date_since are mutually exclusive; specify only one time boundary"
|
||||||
|
.into(),
|
||||||
|
ErrorCode::InvalidParameter
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(date_since) = self.date_since.as_ref() {
|
if let Some(date_since) = self.date_since.as_ref() {
|
||||||
date_since.validate()?;
|
date_since.validate()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(date_before) = self.date_before.as_ref() {
|
||||||
|
date_before.validate_date()?;
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(account.account_type, AccountType::IMAP) {
|
if matches!(account.account_type, AccountType::IMAP) {
|
||||||
if let Some(mailboxes) = self.sync_folders.as_ref() {
|
if let Some(mailboxes) = self.sync_folders.as_ref() {
|
||||||
if mailboxes.is_empty() {
|
if mailboxes.is_empty() {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::error::{code::ErrorCode, BichonResult},
|
modules::error::{code::ErrorCode, BichonResult},
|
||||||
raise_error,
|
raise_error,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ use crate::modules::{
|
|||||||
account::{
|
account::{
|
||||||
entity::ImapConfig,
|
entity::ImapConfig,
|
||||||
migration::{AccountModel, AccountType},
|
migration::{AccountModel, AccountType},
|
||||||
since::DateSince,
|
since::{DateSince, RelativeDate},
|
||||||
},
|
},
|
||||||
users::BichonUser,
|
users::BichonUser,
|
||||||
};
|
};
|
||||||
@@ -39,10 +39,12 @@ pub struct AccountResp {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub capabilities: Option<Vec<String>>,
|
pub capabilities: Option<Vec<String>>,
|
||||||
pub date_since: Option<DateSince>,
|
pub date_since: Option<DateSince>,
|
||||||
|
pub date_before: Option<RelativeDate>,
|
||||||
pub folder_limit: Option<u32>,
|
pub folder_limit: Option<u32>,
|
||||||
pub sync_folders: Option<Vec<String>>,
|
pub sync_folders: Option<Vec<String>>,
|
||||||
pub account_type: AccountType,
|
pub account_type: AccountType,
|
||||||
pub sync_interval_min: Option<i64>,
|
pub sync_interval_min: Option<i64>,
|
||||||
|
pub sync_batch_size: Option<u32>,
|
||||||
pub known_folders: Option<BTreeSet<String>>,
|
pub known_folders: Option<BTreeSet<String>>,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub updated_at: i64,
|
pub updated_at: i64,
|
||||||
@@ -65,10 +67,12 @@ impl AccountResp {
|
|||||||
name: account.name,
|
name: account.name,
|
||||||
capabilities: account.capabilities,
|
capabilities: account.capabilities,
|
||||||
date_since: account.date_since,
|
date_since: account.date_since,
|
||||||
|
date_before: account.date_before,
|
||||||
folder_limit: account.folder_limit,
|
folder_limit: account.folder_limit,
|
||||||
sync_folders: account.sync_folders,
|
sync_folders: account.sync_folders,
|
||||||
account_type: account.account_type,
|
account_type: account.account_type,
|
||||||
sync_interval_min: account.sync_interval_min,
|
sync_interval_min: account.sync_interval_min,
|
||||||
|
sync_batch_size: account.sync_batch_size,
|
||||||
known_folders: account.known_folders,
|
known_folders: account.known_folders,
|
||||||
created_at: account.created_at,
|
created_at: account.created_at,
|
||||||
updated_at: account.updated_at,
|
updated_at: account.updated_at,
|
||||||
|
|||||||
Vendored
+11
-6
@@ -16,7 +16,6 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modules::{
|
modules::{
|
||||||
account::{migration::AccountModel, state::AccountRunningState},
|
account::{migration::AccountModel, state::AccountRunningState},
|
||||||
@@ -37,7 +36,7 @@ use crate::{
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
pub const BATCH_SIZE: u32 = 50;
|
pub const DEFAULT_BATCH_SIZE: u32 = 50;
|
||||||
|
|
||||||
pub async fn fetch_and_save_since_date(
|
pub async fn fetch_and_save_since_date(
|
||||||
account: &AccountModel,
|
account: &AccountModel,
|
||||||
@@ -69,7 +68,11 @@ pub async fn fetch_and_save_since_date(
|
|||||||
|
|
||||||
// let semaphore = Arc::new(Semaphore::new(5));
|
// let semaphore = Arc::new(Semaphore::new(5));
|
||||||
|
|
||||||
let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false);
|
let uid_batches = generate_uid_sequence_hashset(
|
||||||
|
uid_vec,
|
||||||
|
account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) as usize,
|
||||||
|
false,
|
||||||
|
);
|
||||||
AccountRunningState::set_initial_current_syncing_folder(
|
AccountRunningState::set_initial_current_syncing_folder(
|
||||||
account_id,
|
account_id,
|
||||||
mailbox.name.clone(),
|
mailbox.name.clone(),
|
||||||
@@ -105,9 +108,11 @@ pub async fn fetch_and_save_full_mailbox(
|
|||||||
_ => total,
|
_ => total,
|
||||||
};
|
};
|
||||||
let page_size = if let Some(limit) = folder_limit {
|
let page_size = if let Some(limit) = folder_limit {
|
||||||
limit.max(100).min(BATCH_SIZE as u32)
|
limit
|
||||||
|
.max(100)
|
||||||
|
.min(account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE))
|
||||||
} else {
|
} else {
|
||||||
BATCH_SIZE as u32
|
account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE)
|
||||||
};
|
};
|
||||||
|
|
||||||
let total_batches = total_to_fetch.div_ceil(page_size);
|
let total_batches = total_to_fetch.div_ceil(page_size);
|
||||||
@@ -349,7 +354,7 @@ async fn perform_incremental_sync(
|
|||||||
Some(max_uid) => {
|
Some(max_uid) => {
|
||||||
let executor = MAIL_CONTEXT.imap(account.id).await?;
|
let executor = MAIL_CONTEXT.imap(account.id).await?;
|
||||||
executor
|
executor
|
||||||
.fetch_new_mail(account.id, local_mailbox, max_uid + 1)
|
.fetch_new_mail(account, local_mailbox, max_uid + 1)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use crate::modules::account::migration::AccountModel;
|
||||||
use crate::modules::account::state::AccountRunningState;
|
use crate::modules::account::state::AccountRunningState;
|
||||||
use crate::modules::cache::imap::mailbox::MailBox;
|
use crate::modules::cache::imap::mailbox::MailBox;
|
||||||
use crate::modules::cache::imap::sync::flow::{generate_uid_sequence_hashset, BATCH_SIZE};
|
use crate::modules::cache::imap::sync::flow::{generate_uid_sequence_hashset, DEFAULT_BATCH_SIZE};
|
||||||
use crate::modules::envelope::extractor::extract_envelope;
|
use crate::modules::envelope::extractor::extract_envelope;
|
||||||
use crate::modules::error::code::ErrorCode;
|
use crate::modules::error::code::ErrorCode;
|
||||||
use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER};
|
use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER};
|
||||||
@@ -80,7 +81,7 @@ impl ImapExecutor {
|
|||||||
|
|
||||||
pub async fn fetch_new_mail(
|
pub async fn fetch_new_mail(
|
||||||
&self,
|
&self,
|
||||||
account_id: u64,
|
account: &AccountModel,
|
||||||
mailbox: &MailBox,
|
mailbox: &MailBox,
|
||||||
start_uid: u64,
|
start_uid: u64,
|
||||||
) -> BichonResult<()> {
|
) -> BichonResult<()> {
|
||||||
@@ -98,17 +99,21 @@ impl ImapExecutor {
|
|||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"[account {}][mailbox {}] {} envelopes need to be fetched",
|
"[account {}][mailbox {}] {} envelopes need to be fetched",
|
||||||
account_id, mailbox.name, len
|
account.id, mailbox.name, len
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut uid_vec: Vec<u32> = uid_list.into_iter().collect();
|
let mut uid_vec: Vec<u32> = uid_list.into_iter().collect();
|
||||||
uid_vec.sort();
|
uid_vec.sort();
|
||||||
let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false);
|
let uid_batches = generate_uid_sequence_hashset(
|
||||||
|
uid_vec,
|
||||||
|
account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) as usize,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
let too_many = len as u32 > 10 * BATCH_SIZE;
|
let too_many = len as u32 > 5 * account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
|
||||||
if too_many {
|
if too_many {
|
||||||
AccountRunningState::set_initial_current_syncing_folder(
|
AccountRunningState::set_initial_current_syncing_folder(
|
||||||
account_id,
|
account.id,
|
||||||
mailbox.name.clone(),
|
mailbox.name.clone(),
|
||||||
uid_batches.len() as u32,
|
uid_batches.len() as u32,
|
||||||
)
|
)
|
||||||
@@ -118,13 +123,13 @@ impl ImapExecutor {
|
|||||||
for (index, batch) in uid_batches.into_iter().enumerate() {
|
for (index, batch) in uid_batches.into_iter().enumerate() {
|
||||||
if too_many {
|
if too_many {
|
||||||
AccountRunningState::set_current_sync_batch_number(
|
AccountRunningState::set_current_sync_batch_number(
|
||||||
account_id,
|
account.id,
|
||||||
mailbox.name.clone(),
|
mailbox.name.clone(),
|
||||||
(index + 1) as u32,
|
(index + 1) as u32,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
self.uid_batch_retrieve_emails(account_id, mailbox.id, &batch, &mailbox.encoded_name())
|
self.uid_batch_retrieve_emails(account.id, mailbox.id, &batch, &mailbox.encoded_name())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-ace": "^13.0.0",
|
"react-ace": "^13.0.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "9.13.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.54.0",
|
||||||
"react-i18next": "^16.3.5",
|
"react-i18next": "^16.3.5",
|
||||||
|
|||||||
Generated
+25
-8
@@ -153,8 +153,8 @@ importers:
|
|||||||
specifier: ^13.0.0
|
specifier: ^13.0.0
|
||||||
version: 13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
react-day-picker:
|
react-day-picker:
|
||||||
specifier: 8.10.1
|
specifier: 9.13.0
|
||||||
version: 8.10.1(date-fns@3.6.0)(react@18.3.1)
|
version: 9.13.0(react@18.3.1)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
@@ -402,6 +402,9 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@date-fns/tz@1.4.1':
|
||||||
|
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||||
|
|
||||||
'@emotion/babel-plugin@11.13.5':
|
'@emotion/babel-plugin@11.13.5':
|
||||||
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
|
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
|
||||||
|
|
||||||
@@ -3362,9 +3365,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
date-fns-jalali@4.1.0-0:
|
||||||
|
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||||
|
|
||||||
date-fns@3.6.0:
|
date-fns@3.6.0:
|
||||||
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
debug@4.3.7:
|
debug@4.3.7:
|
||||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -4326,11 +4335,11 @@ packages:
|
|||||||
react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
react-day-picker@8.10.1:
|
react-day-picker@9.13.0:
|
||||||
resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
|
resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
date-fns: ^2.28.0 || ^3.0.0
|
react: '>=16.8.0'
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
|
|
||||||
react-dom@18.3.1:
|
react-dom@18.3.1:
|
||||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||||
@@ -5166,6 +5175,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.18
|
'@types/react': 18.3.18
|
||||||
|
|
||||||
|
'@date-fns/tz@1.4.1': {}
|
||||||
|
|
||||||
'@emotion/babel-plugin@11.13.5':
|
'@emotion/babel-plugin@11.13.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.25.9
|
'@babel/helper-module-imports': 7.25.9
|
||||||
@@ -8083,8 +8094,12 @@ snapshots:
|
|||||||
|
|
||||||
d3-timer@3.0.1: {}
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
date-fns-jalali@4.1.0-0: {}
|
||||||
|
|
||||||
date-fns@3.6.0: {}
|
date-fns@3.6.0: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@4.3.7:
|
debug@4.3.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -9262,9 +9277,11 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1):
|
react-day-picker@9.13.0(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 3.6.0
|
'@date-fns/tz': 1.4.1
|
||||||
|
date-fns: 4.1.0
|
||||||
|
date-fns-jalali: 4.1.0-0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
react-dom@18.3.1(react@18.3.1):
|
react-dom@18.3.1(react@18.3.1):
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import axiosInstance from "@/api/axiosInstance";
|
import axiosInstance from "@/api/axiosInstance";
|
||||||
import { AccountModel } from "@/features/accounts/data/schema";
|
|
||||||
import { PaginatedResponse } from "..";
|
import { PaginatedResponse } from "..";
|
||||||
|
|
||||||
export interface MinimalAccount {
|
export interface MinimalAccount {
|
||||||
@@ -56,6 +55,59 @@ export interface MailboxBatchProgress {
|
|||||||
current_batch: number;
|
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) => {
|
export const account_state = async (account_id: number) => {
|
||||||
const response = await axiosInstance.get<AccountRunningState>(`/api/v1/account-state/${account_id}`);
|
const response = await axiosInstance.get<AccountRunningState>(`/api/v1/account-state/${account_id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -52,11 +52,9 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
import { useToast } from '@/hooks/use-toast'
|
||||||
|
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { useRoles } from '@/hooks/use-roles'
|
import { useRoles } from '@/hooks/use-roles'
|
||||||
import { useMinimalUsers } from '@/hooks/use-minimal-users'
|
import { useMinimalUsers } from '@/hooks/use-minimal-users'
|
||||||
import { access_assign } from '@/api/account/api'
|
import { access_assign, AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentRow: AccountModel
|
currentRow: AccountModel
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// 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 { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -34,6 +34,30 @@ interface Props {
|
|||||||
|
|
||||||
export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
||||||
const { t } = useTranslation()
|
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 (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -77,6 +101,10 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
|||||||
<span className="text-muted-foreground">{t('accounts.incrementalSyncInterval')}:</span>
|
<span className="text-muted-foreground">{t('accounts.incrementalSyncInterval')}:</span>
|
||||||
<span>{t('accounts.everyMinutes', { minutes: currentRow.sync_interval_min })}</span>
|
<span>{t('accounts.everyMinutes', { minutes: currentRow.sync_interval_min })}</span>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-muted-foreground">{t('accounts.capabilities')}:</span>
|
<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">
|
<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>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-muted-foreground">{t('accounts.dateSelection')}:</span>
|
<span className="text-muted-foreground">{t('accounts.syncScope')}:</span>
|
||||||
<span>
|
{hasSince && (
|
||||||
{currentRow.date_since?.fixed
|
<div className="flex flex-col">
|
||||||
? t('accounts.since') + ' ' + currentRow.date_since.fixed
|
<span className="text-xs text-muted-foreground">
|
||||||
: currentRow.date_since?.relative
|
{t('accounts.sinceFixed')}:
|
||||||
? t('accounts.recent') + ' ' + currentRow.date_since.relative.value + ' ' + currentRow.date_since.relative.unit
|
</span>
|
||||||
: t('accounts.notAvailable')}
|
<span className="text-sm">{sinceText}</span>
|
||||||
</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>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-muted-foreground">{t('accounts.folderLimit')}:</span>
|
<span className="text-muted-foreground">{t('accounts.folderLimit')}:</span>
|
||||||
@@ -100,8 +147,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Server Configuration Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('accounts.serverConfiguration')}</CardTitle>
|
<CardTitle>{t('accounts.serverConfiguration')}</CardTitle>
|
||||||
@@ -143,8 +188,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Sync Folders Card */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('accounts.syncFoldersTitle')}</CardTitle>
|
<CardTitle>{t('accounts.syncFoldersTitle')}</CardTitle>
|
||||||
|
|||||||
@@ -16,14 +16,12 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Form } from '@/components/ui/form';
|
import { Form } from '@/components/ui/form';
|
||||||
import { AccountModel, ImapConfig } from '../data/schema';
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -31,11 +29,12 @@ import Step1 from './step1';
|
|||||||
import Step2 from './step2';
|
import Step2 from './step2';
|
||||||
import Step3 from './step3';
|
import Step3 from './step3';
|
||||||
import Step4 from './step4';
|
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 { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ToastAction } from '@/components/ui/toast';
|
import { ToastAction } from '@/components/ui/toast';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const encryptionSchema = z.union([
|
const encryptionSchema = z.union([
|
||||||
z.literal('Ssl'),
|
z.literal('Ssl'),
|
||||||
@@ -80,7 +79,7 @@ const getRelativeDateSchema = (t: (key: string) => string) => z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getDateSelectionSchema = (t: (key: string) => string) => z.union([
|
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.object({ relative: getRelativeDateSchema(t) }),
|
||||||
z.undefined(),
|
z.undefined(),
|
||||||
]);
|
]);
|
||||||
@@ -107,8 +106,13 @@ export type Account = {
|
|||||||
value?: number;
|
value?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
date_before?: {
|
||||||
|
unit?: 'Days' | 'Months' | 'Years';
|
||||||
|
value?: number;
|
||||||
|
};
|
||||||
folder_limit?: number;
|
folder_limit?: number;
|
||||||
sync_interval_min: number;
|
sync_interval_min: number;
|
||||||
|
sync_batch_size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
|
const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
|
||||||
@@ -119,12 +123,18 @@ const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
|
|||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
use_dangerous: z.boolean(),
|
use_dangerous: z.boolean(),
|
||||||
date_since: getDateSelectionSchema(t).optional(),
|
date_since: getDateSelectionSchema(t).optional(),
|
||||||
|
date_before: getRelativeDateSchema(t).optional(),
|
||||||
folder_limit: z
|
folder_limit: z
|
||||||
.number({ invalid_type_error: t('validation.folderLimitMustBeNumber') })
|
.number({ invalid_type_error: t('validation.folderLimitMustBeNumber') })
|
||||||
.int()
|
.int()
|
||||||
.min(100, { message: t('validation.folderLimitMustBeAtLeast100') })
|
.min(100, { message: t('validation.folderLimitMustBeAtLeast100') })
|
||||||
.optional(),
|
.optional(),
|
||||||
sync_interval_min: z.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') }).int().min(10, { message: t('validation.incrementalSyncMustBeAtLeast10') }),
|
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 = {
|
type Step = {
|
||||||
@@ -133,14 +143,12 @@ type Step = {
|
|||||||
fields: (keyof Account)[];
|
fields: (keyof Account)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Steps = [
|
export type Steps = [...Step[]];
|
||||||
...Step[]
|
|
||||||
];
|
|
||||||
|
|
||||||
const getSteps = (t: (key: string) => string): Steps => [
|
const getSteps = (t: (key: string) => string): Steps => [
|
||||||
{ id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email"] },
|
{ 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-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: [] },
|
{ id: "step-4", name: t('accounts.steps.summary'), fields: [] },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -168,8 +176,10 @@ const defaultValues: Account = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
use_dangerous: false,
|
use_dangerous: false,
|
||||||
date_since: undefined,
|
date_since: undefined,
|
||||||
|
date_before: undefined,
|
||||||
folder_limit: undefined,
|
folder_limit: undefined,
|
||||||
sync_interval_min: 10,
|
sync_interval_min: 10,
|
||||||
|
sync_batch_size: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyImap: ImapConfig = {
|
const emptyImap: ImapConfig = {
|
||||||
@@ -194,8 +204,10 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => {
|
|||||||
enabled: currentRow.enabled,
|
enabled: currentRow.enabled,
|
||||||
use_dangerous: currentRow.use_dangerous,
|
use_dangerous: currentRow.use_dangerous,
|
||||||
date_since: currentRow.date_since ?? undefined,
|
date_since: currentRow.date_since ?? undefined,
|
||||||
|
date_before: currentRow.date_before ?? undefined,
|
||||||
folder_limit: currentRow.folder_limit ?? undefined,
|
folder_limit: currentRow.folder_limit ?? undefined,
|
||||||
sync_interval_min: currentRow.sync_interval_min ?? 10,
|
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,
|
enabled: data.enabled,
|
||||||
use_dangerous: data.use_dangerous,
|
use_dangerous: data.use_dangerous,
|
||||||
date_since: data.date_since,
|
date_since: data.date_since,
|
||||||
|
date_before: data.date_before,
|
||||||
folder_limit: data.folder_limit,
|
folder_limit: data.folder_limit,
|
||||||
sync_interval_min: data.sync_interval_min,
|
sync_interval_min: data.sync_interval_min,
|
||||||
|
sync_batch_size: data.sync_batch_size,
|
||||||
};
|
};
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
updateMutation.mutate(commonData);
|
updateMutation.mutate(commonData);
|
||||||
@@ -330,61 +344,67 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(state) => {
|
onOpenChange={(state) => {
|
||||||
form.reset();
|
if (!state) {
|
||||||
setCurrentStep(1);
|
form.reset();
|
||||||
|
setCurrentStep(1);
|
||||||
|
}
|
||||||
onOpenChange(state);
|
onOpenChange(state);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className='max-w-5xl'>
|
<DialogContent className="max-w-[95vw] md:max-w-5xl w-full p-0 overflow-hidden flex flex-col h-[90vh]">
|
||||||
<DialogHeader className='text-left mb-4'>
|
<div className="p-6 pb-2 flex-shrink-0">
|
||||||
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
|
<DialogHeader className="text-left">
|
||||||
<DialogDescription>
|
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
|
||||||
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
|
<DialogDescription>
|
||||||
{t('accounts.clickSaveWhenDone')}
|
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
|
||||||
</DialogDescription>
|
{t('accounts.clickSaveWhenDone')}
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
<ScrollArea className="h-[38rem] w-full pr-4 -mr-4 py-1">
|
</DialogHeader>
|
||||||
<>
|
</div>
|
||||||
<div className="flex my-5 space-x-4 md:hidden">
|
|
||||||
{steps.map((step, index) => (
|
<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
|
<Button
|
||||||
key={step.id}
|
variant={currentStep === index + 1 ? "default" : "secondary"}
|
||||||
className={`size-9 rounded-full border font-bold ${currentStep === index + 1 ? "bg-primary text-white" : "bg-gray-200 text-black"
|
className="size-8 rounded-full font-bold p-0"
|
||||||
}`}
|
|
||||||
disabled={currentStep === index + 1}
|
disabled={currentStep === index + 1}
|
||||||
onClick={() => setCurrentStep(index + 1)}
|
onClick={() => setCurrentStep(index + 1)}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
<span className="text-[10px] mt-1 text-muted-foreground line-clamp-1">{step.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-full p-4">
|
))}
|
||||||
<div className="flex md:h-min rounded-xl md:rounded-2xl p-4">
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<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 {...form}>
|
||||||
<form
|
<form id="account-register-form" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
id="account-register-form"
|
|
||||||
className="flex-grow flex flex-col px-4 md:px-8 lg:px-12 ml-[240px]"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
{currentStep === 1 && <Step1 isEdit={isEdit} />}
|
{currentStep === 1 && <Step1 isEdit={isEdit} />}
|
||||||
{currentStep === 2 && <Step2 isEdit={isEdit} />}
|
{currentStep === 2 && <Step2 isEdit={isEdit} />}
|
||||||
{currentStep === 3 && <Step3 />}
|
{currentStep === 3 && <Step3 />}
|
||||||
@@ -392,14 +412,16 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
</>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
<DialogFooter className="flex flex-wrap gap-2">
|
|
||||||
|
<DialogFooter className="p-4 md:p-6 bg-background flex flex-row sm:justify-end gap-2 flex-shrink-0">
|
||||||
{currentStep > 1 && (
|
{currentStep > 1 && (
|
||||||
<Button
|
<Button
|
||||||
type="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)}
|
onClick={() => setCurrentStep(currentStep - 1)}
|
||||||
>
|
>
|
||||||
{t('accounts.goBack')}
|
{t('accounts.goBack')}
|
||||||
@@ -408,7 +430,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
{currentStep < LAST_STEP && (
|
{currentStep < LAST_STEP && (
|
||||||
<Button
|
<Button
|
||||||
type="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}
|
onClick={handleContinue}
|
||||||
>
|
>
|
||||||
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
|
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
|
||||||
@@ -418,7 +440,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="account-register-form"
|
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')}
|
{isEdit ? t('accounts.saveChanges') : t('accounts.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
import { ColumnDef } from '@tanstack/react-table'
|
import { ColumnDef } from '@tanstack/react-table'
|
||||||
import LongText from '@/components/long-text'
|
import LongText from '@/components/long-text'
|
||||||
|
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { DataTableColumnHeader } from './data-table-column-header'
|
import { DataTableColumnHeader } from './data-table-column-header'
|
||||||
import { DataTableRowActions } from './data-table-row-actions'
|
import { DataTableRowActions } from './data-table-row-actions'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
@@ -28,6 +26,7 @@ import { OAuth2Action } from './oauth2-action'
|
|||||||
import { RunningStateCellAction } from './running-state-action'
|
import { RunningStateCellAction } from './running-state-action'
|
||||||
import { EnableAction } from './enable-action'
|
import { EnableAction } from './enable-action'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
export function useColumns(): ColumnDef<AccountModel>[] {
|
export function useColumns(): ColumnDef<AccountModel>[] {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -112,7 +111,7 @@ export function useColumns(): ColumnDef<AccountModel>[] {
|
|||||||
{
|
{
|
||||||
accessorKey: 'created_by',
|
accessorKey: 'created_by',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title="Owner" className="justify-center" />
|
<DataTableColumnHeader column={column} title={t('accounts.owner')} className="justify-center" />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { created_user_name, created_user_email } = row.original;
|
const { created_user_name, created_user_email } = row.original;
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { useAccountContext } from '../context'
|
import { useAccountContext } from '../context'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { Mailbox, MessageSquareMore } from 'lucide-react'
|
import { Mailbox, MessageSquareMore } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
interface DataTableRowActionsProps {
|
interface DataTableRowActionsProps {
|
||||||
row: Row<AccountModel>
|
row: Row<AccountModel>
|
||||||
@@ -113,7 +113,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
setOpen('access-assign')
|
setOpen('access-assign')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Access Control</span>
|
<span>{t('accounts.accessControl')}</span>
|
||||||
<DropdownMenuShortcut>
|
<DropdownMenuShortcut>
|
||||||
<IconShieldLock size={16} />
|
<IconShieldLock size={16} />
|
||||||
</DropdownMenuShortcut>
|
</DropdownMenuShortcut>
|
||||||
|
|||||||
@@ -24,11 +24,10 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { remove_account } from '@/api/account/api'
|
import { AccountModel, remove_account } from '@/api/account/api'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -18,13 +18,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import { Row } from '@tanstack/react-table'
|
import { Row } from '@tanstack/react-table'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
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 { toast } from '@/hooks/use-toast'
|
||||||
import { AxiosError } from 'axios'
|
import { AxiosError } from 'axios'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|||||||
@@ -28,12 +28,11 @@ import { AxiosError } from 'axios';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { AccountModel } from '../data/schema';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
import { Row } from '@tanstack/react-table'
|
import { Row } from '@tanstack/react-table'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useAccountContext } from '../context'
|
import { useAccountContext } from '../context'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
interface DataTableRowActionsProps {
|
interface DataTableRowActionsProps {
|
||||||
row: Row<AccountModel>
|
row: Row<AccountModel>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { get_oauth2_tokens } from '@/api/oauth2/api'
|
import { get_oauth2_tokens } from '@/api/oauth2/api'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
@@ -44,6 +43,7 @@ import { ToastAction } from '@/components/ui/toast'
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
import { dateFnsLocaleMap } from '@/lib/utils'
|
import { dateFnsLocaleMap } from '@/lib/utils'
|
||||||
import { enUS } from 'date-fns/locale'
|
import { enUS } from 'date-fns/locale'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentRow: AccountModel
|
currentRow: AccountModel
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
import { Row } from '@tanstack/react-table'
|
import { Row } from '@tanstack/react-table'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AccountModel } from '../data/schema';
|
|
||||||
import { useAccountContext } from '../context';
|
import { useAccountContext } from '../context';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCurrentUser } from '@/hooks/use-current-user';
|
import { useCurrentUser } from '@/hooks/use-current-user';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { ToastAction } from '@/components/ui/toast';
|
import { ToastAction } from '@/components/ui/toast';
|
||||||
|
import { AccountModel } from '@/api/account/api';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: Row<AccountModel>
|
row: Row<AccountModel>
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
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 { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|||||||
@@ -39,189 +39,206 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CalendarIcon } from "lucide-react";
|
import { CalendarIcon } from "lucide-react";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, dateFnsLocaleMap } from "@/lib/utils";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { useTranslation } from "react-i18next";
|
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() {
|
export default function Step3() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control, getValues, setValue } = useFormContext<Account>();
|
const { control, getValues, setValue } = useFormContext<Account>();
|
||||||
const current = getValues();
|
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 (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<FormField
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
control={control}
|
<FormField
|
||||||
name="sync_interval_min"
|
control={control}
|
||||||
render={({ field }) => (
|
name="sync_interval_min"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel className="flex items-center justify-between">
|
<FormItem>
|
||||||
{t('accounts.incrementalSync')}:
|
<FormLabel>{t('accounts.incrementalSync')}</FormLabel>
|
||||||
</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||||
<Input
|
</FormControl>
|
||||||
type="number"
|
<FormMessage />
|
||||||
placeholder={t('accounts.incrementalSyncPlaceholder')}
|
<FormDescription>
|
||||||
{...field}
|
{t('accounts.incrementalSyncDescription')}
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
|
</FormDescription>
|
||||||
/>
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<FormMessage />
|
/>
|
||||||
</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
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start gap-y-1">
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow-sm">
|
||||||
<FormLabel>{t('accounts.enabled')}:</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Checkbox
|
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
||||||
className="mt-2"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</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>
|
</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>
|
<hr className="my-4" />
|
||||||
{t('accounts.syncStartDateDescription', {
|
<div className="space-y-4">
|
||||||
fixedPart: rangeType === 'fixed' ? t('accounts.syncAfterDate') : t('accounts.syncRecentData'),
|
<FormItem>
|
||||||
})}
|
<FormLabel className="text-base font-semibold">{t('accounts.syncScope', 'Sync Strategy')}</FormLabel>
|
||||||
</FormDescription>
|
<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' && (
|
{(syncMode === 'since_relative' || syncMode === 'before_relative') && (
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row items-end gap-4 animate-in fade-in slide-in-from-left-2">
|
||||||
<div className="flex-1">
|
<FormField
|
||||||
<FormField
|
control={control}
|
||||||
control={control}
|
name={syncMode === 'since_relative' ? "date_since.relative.value" : "date_before.value"}
|
||||||
name="date_since.relative.value"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem className="flex-1 max-w-[150px]">
|
||||||
<FormItem>
|
<FormLabel>{t('accounts.duration', 'Duration')}</FormLabel>
|
||||||
<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}>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
|
||||||
<SelectValue placeholder={t('accounts.selectUnit')} />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
|
</FormItem>
|
||||||
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
|
)}
|
||||||
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
|
/>
|
||||||
</SelectContent>
|
<FormField
|
||||||
</Select>
|
control={control}
|
||||||
<FormMessage />
|
name={syncMode === 'since_relative' ? "date_since.relative.unit" : "date_before.unit"}
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem className="w-[180px]">
|
||||||
/>
|
<FormLabel>{t('accounts.unit', 'Unit')}</FormLabel>
|
||||||
</div>
|
<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>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
<hr className="my-4" />
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={control}
|
control={control}
|
||||||
name="folder_limit"
|
name="folder_limit"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex items-center justify-between">{t('accounts.folderLimit')}:</FormLabel>
|
<FormLabel>{t('accounts.folderLimit')}</FormLabel>
|
||||||
<FormDescription>{t('accounts.folderLimitDescription')}</FormDescription>
|
<FormDescription>{t('accounts.folderLimitDescription')}</FormDescription>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -237,4 +254,4 @@ export default function Step3() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,28 @@ export default function Step4() {
|
|||||||
const { getValues } = useFormContext<Account>();
|
const { getValues } = useFormContext<Account>();
|
||||||
const summaryData = getValues();
|
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 (
|
return (
|
||||||
<div className="p-5 rounded-xl">
|
<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">
|
<AccordionItem key="email" value="email">
|
||||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.email')}:</AccordionTrigger>
|
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.email')}:</AccordionTrigger>
|
||||||
<AccordionContent>{summaryData.email}</AccordionContent>
|
<AccordionContent>{summaryData.email}</AccordionContent>
|
||||||
@@ -82,17 +101,44 @@ export default function Step4() {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem key="date_since" value="date_since">
|
<AccordionItem key="sync_scope" value="sync_scope">
|
||||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.dateSelection')}:</AccordionTrigger>
|
<AccordionTrigger className="font-medium capitalize text-gray-600">
|
||||||
<AccordionContent>
|
{t('accounts.syncScope')}:
|
||||||
{summaryData.date_since?.fixed
|
</AccordionTrigger>
|
||||||
? t('accounts.since') + ' ' + summaryData.date_since.fixed
|
|
||||||
: summaryData.date_since?.relative && summaryData.date_since.relative.value && summaryData.date_since.relative.unit
|
<AccordionContent className="space-y-3">
|
||||||
? t('accounts.recent') + ' ' + summaryData.date_since.relative.value + ' ' + summaryData.date_since.relative.unit
|
{hasSince && (
|
||||||
: t('accounts.notAvailable')}
|
<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>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
|
|
||||||
<AccordionItem key="folder_limit" value="folder_limit">
|
<AccordionItem key="folder_limit" value="folder_limit">
|
||||||
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.folderLimit')}:</AccordionTrigger>
|
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.folderLimit')}:</AccordionTrigger>
|
||||||
<AccordionContent>{summaryData.folder_limit ?? t('accounts.notAvailable')}</AccordionContent>
|
<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>
|
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.incrementalSync')}:</AccordionTrigger>
|
||||||
<AccordionContent>{summaryData.sync_interval_min} {t('accounts.minutes')}</AccordionContent>
|
<AccordionContent>{summaryData.sync_interval_min} {t('accounts.minutes')}</AccordionContent>
|
||||||
</AccordionItem>
|
</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>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,12 +29,11 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Loader2, CheckSquare, Square } from 'lucide-react'
|
import { Loader2, CheckSquare, Square } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
|
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
|
||||||
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
|
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
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 { ToastAction } from '@/components/ui/toast'
|
||||||
import axios, { AxiosError } from 'axios'
|
import axios, { AxiosError } from 'axios'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
import { DataTablePagination } from './data-table-pagination'
|
import { DataTablePagination } from './data-table-pagination'
|
||||||
import { DataTableToolbar } from './data-table-toolbar'
|
import { DataTableToolbar } from './data-table-toolbar'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { AccountModel } from '@/api/account/api'
|
||||||
|
|
||||||
declare module '@tanstack/react-table' {
|
declare module '@tanstack/react-table' {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import { AccountModel } from '@/api/account/api';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { AccountModel } from '../data/schema'
|
|
||||||
|
|
||||||
export type AccountDialogType =
|
export type AccountDialogType =
|
||||||
| 'add-imap'
|
| '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'
|
} from './context'
|
||||||
import { MoreVertical, Plus } from 'lucide-react'
|
import { MoreVertical, Plus } from 'lucide-react'
|
||||||
import Logo from '@/assets/logo.svg'
|
import Logo from '@/assets/logo.svg'
|
||||||
import { AccountModel } from './data/schema'
|
|
||||||
import { AccountDetailDrawer } from './components/account-detail'
|
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 { TableSkeleton } from '@/components/table-skeleton'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { OAuth2TokensDialog } from './components/oauth2-tokens'
|
import { OAuth2TokensDialog } from './components/oauth2-tokens'
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "إصدار النظام"
|
"systemVersion": "إصدار النظام"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
"beforeRelativeValue": "مزامنة رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت",
|
||||||
|
"sinceRelativeValue": "مزامنة رسائل البريد الإلكتروني لآخر {{value}} {{unit}}",
|
||||||
|
"syncBatchSize": "حجم دفعة المزامنة",
|
||||||
|
"syncBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP",
|
||||||
|
"incrementalSyncDescription": "عدد مرات إجراء مزامنة البريد الإلكتروني المتزايدة (بالدقائق)",
|
||||||
|
"syncScope": "استراتيجية المزامنة",
|
||||||
|
"syncScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وأرشفتها.",
|
||||||
|
"selectMode": "حدد وضع التصفية",
|
||||||
|
"syncAll": "مزامنة جميع رسائل البريد الإلكتروني",
|
||||||
|
"sinceFixed": "منذ تاريخ محدد",
|
||||||
|
"sinceRelative": "مزامنة رسائل البريد الإلكتروني الحديثة فقط",
|
||||||
|
"beforeRelative": "أرشفة رسائل البريد الإلكتروني القديمة فقط",
|
||||||
|
"duration": "المدة",
|
||||||
|
"unit": "الوحدة",
|
||||||
|
"accessControl": "التحكم في الوصول",
|
||||||
|
"owner": "المنشئ",
|
||||||
"access_control": {
|
"access_control": {
|
||||||
"title": "تخصيص الوصول للحساب",
|
"title": "تخصيص الوصول للحساب",
|
||||||
"description": "تعيين الأدوار والمستخدمين المفوضين لـ {{email}}",
|
"description": "تعيين الأدوار والمستخدمين المفوضين لـ {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Systemversion"
|
"systemVersion": "Systemversion"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Tildeling af kontoadgang",
|
"title": "Tildeling af kontoadgang",
|
||||||
"description": "Tildel roller og autoriserede brugere til {{email}}",
|
"description": "Tildel roller og autoriserede brugere til {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Systemversion"
|
"systemVersion": "Systemversion"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Kontozugriffszuweisung",
|
"title": "Kontozugriffszuweisung",
|
||||||
"description": "Rollen und autorisierte Benutzer für {{email}} zuweisen",
|
"description": "Rollen und autorisierte Benutzer für {{email}} zuweisen",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "System Version"
|
"systemVersion": "System Version"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Account Access Assignment",
|
"title": "Account Access Assignment",
|
||||||
"description": "Assign roles and authorized users to {{email}}",
|
"description": "Assign roles and authorized users to {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Versión del sistema"
|
"systemVersion": "Versión del sistema"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Asignación de Acceso a la Cuenta",
|
"title": "Asignación de Acceso a la Cuenta",
|
||||||
"description": "Asignar roles y usuarios autorizados a {{email}}",
|
"description": "Asignar roles y usuarios autorizados a {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Järjestelmäversio"
|
"systemVersion": "Järjestelmäversio"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Tilin pääsynhallinta",
|
"title": "Tilin pääsynhallinta",
|
||||||
"description": "Määritä roolit ja valtuutetut käyttäjät kohteelle {{email}}",
|
"description": "Määritä roolit ja valtuutetut käyttäjät kohteelle {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Version du système"
|
"systemVersion": "Version du système"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Attribution d'accès au compte",
|
"title": "Attribution d'accès au compte",
|
||||||
"description": "Attribuer des rôles et des utilisateurs autorisés à {{email}}",
|
"description": "Attribuer des rôles et des utilisateurs autorisés à {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Versione del sistema"
|
"systemVersion": "Versione del sistema"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Assegnazione Accesso Account",
|
"title": "Assegnazione Accesso Account",
|
||||||
"description": "Assegna ruoli e utenti autorizzati a {{email}}",
|
"description": "Assegna ruoli e utenti autorizzati a {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "システムバージョン"
|
"systemVersion": "システムバージョン"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "アカウントアクセス割り当て",
|
"title": "アカウントアクセス割り当て",
|
||||||
"description": "{{email}} にロールと権限ユーザーを割り当てます",
|
"description": "{{email}} にロールと権限ユーザーを割り当てます",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "시스템 버전"
|
"systemVersion": "시스템 버전"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
"beforeRelativeValue": "{{value}} {{unit}} 전 이전 이메일 동기화",
|
||||||
|
"sinceRelativeValue": "지난 {{value}} {{unit}} 동안의 이메일 동기화",
|
||||||
|
"syncBatchSize": "동기화 배치 크기",
|
||||||
|
"syncBatchSizeDescription": "IMAP 요청당 가져올 메시지 수",
|
||||||
|
"incrementalSyncDescription": "증분 이메일 동기화 수행 빈도 (분 단위)",
|
||||||
|
"syncScope": "동기화 전략",
|
||||||
|
"syncScopeDescription": "인덱싱 및 아카이빙할 이메일을 선택하십시오.",
|
||||||
|
"selectMode": "필터 모드 선택",
|
||||||
|
"syncAll": "모든 이메일 동기화",
|
||||||
|
"sinceFixed": "특정 날짜 이후",
|
||||||
|
"sinceRelative": "최신 이메일만 동기화",
|
||||||
|
"beforeRelative": "오래된 이메일만 아카이브",
|
||||||
|
"duration": "기간",
|
||||||
|
"unit": "단위",
|
||||||
|
"accessControl": "액세스 제어",
|
||||||
|
"owner": "생성자",
|
||||||
"access_control": {
|
"access_control": {
|
||||||
"title": "계정 액세스 할당",
|
"title": "계정 액세스 할당",
|
||||||
"description": "{{email}}에 역할 및 권한 사용자를 할당합니다",
|
"description": "{{email}}에 역할 및 권한 사용자를 할당합니다",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Systeemversie"
|
"systemVersion": "Systeemversie"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Toewijzing accounttoegang",
|
"title": "Toewijzing accounttoegang",
|
||||||
"description": "Rollen en geautoriseerde gebruikers toewijzen aan {{email}}",
|
"description": "Rollen en geautoriseerde gebruikers toewijzen aan {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Systemversjon"
|
"systemVersion": "Systemversjon"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Tildeling av kontotilgang",
|
"title": "Tildeling av kontotilgang",
|
||||||
"description": "Tildel roller og autoriserte brukere til {{email}}",
|
"description": "Tildel roller og autoriserte brukere til {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Wersja systemu"
|
"systemVersion": "Wersja systemu"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Przypisywanie dostępu do konta",
|
"title": "Przypisywanie dostępu do konta",
|
||||||
"description": "Przypisz role i uprawnionych użytkowników dla {{email}}",
|
"description": "Przypisz role i uprawnionych użytkowników dla {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Versão do sistema"
|
"systemVersion": "Versão do sistema"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Atribuição de Acesso à Conta",
|
"title": "Atribuição de Acesso à Conta",
|
||||||
"description": "Atribuir funções e usuários autorizados a {{email}}",
|
"description": "Atribuir funções e usuários autorizados a {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Версия системы"
|
"systemVersion": "Версия системы"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
"beforeRelativeValue": "Синхронизировать письма старее, чем {{value}} {{unit}} назад",
|
||||||
|
"sinceRelativeValue": "Синхронизировать письма за последние {{value}} {{unit}}",
|
||||||
|
"syncBatchSize": "Размер пакета синхронизации",
|
||||||
|
"syncBatchSizeDescription": "Количество сообщений, получаемых за один запрос IMAP",
|
||||||
|
"incrementalSyncDescription": "Частота инкрементной синхронизации почты (в минутах)",
|
||||||
|
"syncScope": "Стратегия синхронизации",
|
||||||
|
"syncScopeDescription": "Выберите письма для индексации и архивации.",
|
||||||
|
"selectMode": "Выберите режим фильтрации",
|
||||||
|
"syncAll": "Синхронизировать все письма",
|
||||||
|
"sinceFixed": "С определенной даты",
|
||||||
|
"sinceRelative": "Синхронизировать только новые письма",
|
||||||
|
"beforeRelative": "Архивировать только старые письма",
|
||||||
|
"duration": "Продолжительность",
|
||||||
|
"unit": "Единица",
|
||||||
|
"accessControl": "Контроль доступа",
|
||||||
|
"owner": "Создатель",
|
||||||
"access_control": {
|
"access_control": {
|
||||||
"title": "Назначение доступа к аккаунту",
|
"title": "Назначение доступа к аккаунту",
|
||||||
"description": "Назначить роли и авторизованных пользователей для {{email}}",
|
"description": "Назначить роли и авторизованных пользователей для {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "Systemversion"
|
"systemVersion": "Systemversion"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"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": {
|
"access_control": {
|
||||||
"title": "Tilldelning av kontotillgång",
|
"title": "Tilldelning av kontotillgång",
|
||||||
"description": "Tilldela roller och auktoriserade användare till {{email}}",
|
"description": "Tilldela roller och auktoriserade användare till {{email}}",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "系統版本"
|
"systemVersion": "系統版本"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的郵件",
|
||||||
|
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 內的郵件",
|
||||||
|
"syncBatchSize": "批次同步數量",
|
||||||
|
"syncBatchSizeDescription": "每次 IMAP 請求獲取的郵件數量",
|
||||||
|
"incrementalSyncDescription": "執行增量郵件同步的頻率(分鐘)",
|
||||||
|
"syncScope": "同步策略",
|
||||||
|
"syncScopeDescription": "選擇哪些郵件需要被索引和歸檔。",
|
||||||
|
"selectMode": "選擇過濾模式",
|
||||||
|
"syncAll": "同步所有郵件",
|
||||||
|
"sinceFixed": "從特定日期開始 (至今)",
|
||||||
|
"sinceRelative": "僅同步最近的郵件",
|
||||||
|
"beforeRelative": "僅封存舊郵件",
|
||||||
|
"duration": "時長",
|
||||||
|
"unit": "單位",
|
||||||
|
"accessControl": "訪問控制",
|
||||||
|
"owner": "建立者",
|
||||||
"access_control": {
|
"access_control": {
|
||||||
"title": "帳戶訪問分配",
|
"title": "帳戶訪問分配",
|
||||||
"description": "為 {{email}} 分配角色和授權用戶",
|
"description": "為 {{email}} 分配角色和授權用戶",
|
||||||
|
|||||||
@@ -138,6 +138,22 @@
|
|||||||
"systemVersion": "系统版本"
|
"systemVersion": "系统版本"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
|
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的邮件",
|
||||||
|
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 内的邮件",
|
||||||
|
"syncBatchSize": "批次同步数量",
|
||||||
|
"syncBatchSizeDescription": "每次 IMAP 请求获取的邮件数量",
|
||||||
|
"incrementalSyncDescription": "执行增量邮件同步的频率(分钟)",
|
||||||
|
"syncScope": "同步策略",
|
||||||
|
"syncScopeDescription": "选择哪些邮件需要被索引和归档。",
|
||||||
|
"selectMode": "选择过滤模式",
|
||||||
|
"syncAll": "同步所有邮件",
|
||||||
|
"sinceFixed": "从特定日期开始 (至今)",
|
||||||
|
"sinceRelative": "仅同步最近的邮件 (相对时间)",
|
||||||
|
"beforeRelative": "仅同步旧邮件",
|
||||||
|
"duration": "时长",
|
||||||
|
"unit": "单位",
|
||||||
|
"accessControl": "访问控制",
|
||||||
|
"owner": "创建者",
|
||||||
"access_control": {
|
"access_control": {
|
||||||
"title": "账户访问分配",
|
"title": "账户访问分配",
|
||||||
"description": "为 {{email}} 分配角色和授权用户",
|
"description": "为 {{email}} 分配角色和授权用户",
|
||||||
|
|||||||
Reference in New Issue
Block a user