Compare commits

...
12 Commits
45 changed files with 1781 additions and 867 deletions
+21
View File
@@ -0,0 +1,21 @@
---
name: Bug Report
about: Report a problem you encountered
title: "[BUG] "
labels: ["bug"]
assignees: ""
---
> **Please write and communicate in English.**
### Version
Which version are you using?
### Steps to Reproduce
Describe the steps to reproduce the issue clearly.
### Issue Description
What is the problem you encountered?
### Screenshots or Logs (optional)
Attach any screenshots or logs if available.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+18
View File
@@ -0,0 +1,18 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[FEATURE] "
labels: ["enhancement"]
assignees: ""
---
> **Please write and communicate in English.**
### Description
What feature would you like to see?
### Purpose / Use Case
Why is this feature needed? What problem does it solve?
### Additional Information (optional)
Any extra ideas or context.
+15
View File
@@ -0,0 +1,15 @@
---
name: Other Issue
about: Any other question or topic
title: ""
labels: ["question"]
assignees: ""
---
> **Please write and communicate in English.**
### Description
Describe your question or topic.
### Additional Information (optional)
Provide any additional context if needed.
Generated
+7 -7
View File
@@ -424,7 +424,7 @@ dependencies = [
[[package]]
name = "bichon"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"ahash",
"async-imap",
@@ -3536,9 +3536,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"web-time",
"zeroize",
@@ -4633,9 +4633,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -4709,9 +4709,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "bichon"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
[[bin]]
@@ -39,9 +39,9 @@ ring = { version = "0.17.14", features = ["std"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1.41"
tracing = "0.1.43"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
base64 = "0.22.1"
snafu = "0.8.9"
reqwest = { version = "0.12.24", default-features = false, features = [
@@ -83,7 +83,7 @@ async-imap = { version = "0.11.1", default-features = false, features = [
] }
webpki-roots = "1.0.4"
rustls = { version = "0.23.35", default-features = false, features = ["ring"] }
rustls-pki-types = "1.13.0"
rustls-pki-types = "1.13.1"
tokio-io-timeout = "1.2.1"
bb8 = "0.9.1"
semver = "1.0.27"
+18 -7
View File
@@ -124,6 +124,12 @@ Its not perfect, but I hope it brings you value.
<img width="1920" height="910" alt="image" src="https://github.com/user-attachments/assets/14561b74-ed53-4017-9c5b-a64920ec3526" />
<img width="1913" height="909" alt="image" src="https://github.com/user-attachments/assets/6fd54cb0-c86f-4ceb-a955-c81107614fc4" />
<img width="1916" height="814" alt="image" src="https://github.com/user-attachments/assets/6a079d98-ff6c-46f4-9ec6-e76d320bff5d" />
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=rustmailer/bichon&type=date&legend=top-left)](https://www.star-history.com/#rustmailer/bichon&type=date&legend=top-left)
## 🚀 Quick Start
### Docker Deployment (Recommended)
@@ -293,10 +299,14 @@ You can change the password via the WebUI:
- Used exclusively for lightweight configuration and account metadata.
- **Email Protocols**: IMAP (Supports standard Password & OAuth2)
## 🤝 Contributing
Issues and Pull Requests are welcome!
Contributions of all kinds are welcome!
Whether youd like to submit code, report a bug, or share practical suggestions that can help improve the project, your input is highly appreciated.
Feel free to open an Issue or a Pull Request anytime. You can also reach out on Discord if youd like to discuss ideas or improvements.
<a href="https://discord.gg/evFnSpdpaE">
<img src="https://img.shields.io/badge/Discord-Join%20Server-7289DA?logo=discord&logoColor=white" alt="Discord">
</a>
## 🧑‍💻 Developer Guide
@@ -313,7 +323,7 @@ To build or contribute to Bichon, the following environment is recommended:
```bash
git clone https://github.com/rustmailer/bichon.git
cd bichon
````
```
#### 2. Build the WebUI
@@ -363,10 +373,11 @@ This project is licensed under [AGPLv3](LICENSE).
- [Discord](https://discord.gg/evFnSpdpaE)
## Get Involved
## 💖 Support & Promotion
If you enjoy using Bichon, youre welcome to help spread the word 😄
Feel free to share your experience on communities like Reddits r/selfhosted, r/opensource, or elsewhere—installation tips, challenges you ran into, or cool features you discovered.
Completely voluntary—just sharing your experience helps more people discover Bichon!
If this project has been helpful to you and youd like to support its development, you can consider making a small donation or helping spread the word.
Financial support is optional but deeply appreciated — it helps me dedicate more time and resources to building new features and improving the overall experience.
You can also support the project by sharing it with others, writing about your experience, or recommending it within relevant communities. Every bit of visibility helps more people benefit from the tool!
[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-Support%20the%20Project-FFDD00?logo=buy-me-a-coffee)](https://buymeacoffee.com/rustmailer)
+7 -6
View File
@@ -16,7 +16,7 @@
// 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/>.
use crate::modules::account::migration::AccountType;
use crate::modules::context::Initialize;
use crate::modules::error::code::ErrorCode;
use crate::raise_error;
@@ -33,8 +33,7 @@ use dashmap::DashMap;
use std::sync::{Arc, LazyLock};
use tracing::info;
pub static MAIL_CONTEXT: LazyLock<EmailClientExecutors> =
LazyLock::new(EmailClientExecutors::new);
pub static MAIL_CONTEXT: LazyLock<EmailClientExecutors> = LazyLock::new(EmailClientExecutors::new);
pub struct EmailClientExecutors {
start_at: i64,
@@ -88,15 +87,17 @@ impl EmailClientExecutors {
pub async fn start_account_syncers(&self) -> BichonResult<()> {
let accounts = AccountModel::list_all().await?;
let active_accounts: Vec<AccountModel> =
accounts.into_iter().filter(|a| a.enabled).collect();
let active_accounts: Vec<AccountModel> = accounts
.into_iter()
.filter(|a| a.enabled && matches!(a.account_type, AccountType::IMAP))
.collect();
if active_accounts.is_empty() {
info!("No active accounts found for account initialization.");
return Ok(());
}
info!(
"System has {} active accounts to initialize.",
"System has {} active IMAP accounts to initialize.",
active_accounts.len()
);
for account in active_accounts {
+5 -1
View File
@@ -16,12 +16,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use tantivy::{schema::Value, TantivyDocument};
use crate::{
bichon_version,
modules::{
account::migration::AccountModel,
error::{code::ErrorCode, BichonResult},
@@ -45,6 +45,8 @@ pub struct DashboardStats {
pub with_attachment_count: u64, // Emails with attachments
pub without_attachment_count: u64, // Emails without attachments
pub top_largest_emails: Vec<LargestEmail>, // Top 10 largest emails
pub system_version: String, // The semantic version string of the currently running backend service
pub commit_hash: String, // Git commit hash used to build this system version
}
impl DashboardStats {
@@ -57,6 +59,8 @@ impl DashboardStats {
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
stat.index_usage_bytes = get_total_size(&DATA_DIR_MANAGER.envelope_dir)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
stat.system_version = bichon_version!().to_string();
stat.commit_hash = env!("GIT_HASH").to_string();
Ok(stat)
}
}
+87 -2
View File
@@ -16,7 +16,6 @@
// 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/>.
use crate::modules::common::AddrVec;
use crate::modules::error::code::ErrorCode;
use crate::modules::error::BichonResult;
@@ -33,12 +32,12 @@ pub fn extract_envelope(fetch: &Fetch, account_id: u64, mailbox_id: u64) -> Bich
.map(|d| d.timestamp_millis())
.unwrap_or(0);
let uid = fetch.uid.unwrap_or(0);
let size = fetch.size.unwrap_or(0);
let body = fetch
.body()
.ok_or_else(|| raise_error!("No body available".into(), ErrorCode::InternalError))?;
let size = fetch.size.unwrap_or(body.len() as u32);
let message = MessageParser::new().parse(body).ok_or_else(|| {
raise_error!(
"Email header parse result is not available".into(),
@@ -118,6 +117,92 @@ pub fn extract_envelope(fetch: &Fetch, account_id: u64, mailbox_id: u64) -> Bich
Ok(envelope)
}
pub fn extract_envelope_from_eml(
body: &[u8],
account_id: u64,
mailbox_id: u64,
) -> BichonResult<Envelope> {
let uid = 0;
let size = body.len() as u32;
let message = MessageParser::new().parse(body).ok_or_else(|| {
raise_error!(
"Email header parse result is not available".into(),
ErrorCode::InternalError
)
})?;
let text = if let Some(text) = message.body_text(0).map(|cow| cow.into_owned()) {
text
} else if let Some(html) = message.body_html(0).map(|cow| cow.into_owned()) {
from_read(html.as_bytes(), 0)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?
} else {
String::new()
};
let message_id = message
.message_id()
.map(String::from)
.unwrap_or(generate_message_id());
let in_reply_to = message.in_reply_to().as_text().map(String::from);
let references = extract_references(&message);
let thread_id = compute_thread_id(in_reply_to, references, &message_id);
let subject = message.subject().map(String::from).unwrap_or("".into());
let date = message.date().map(|d| d.to_timestamp() * 1000).unwrap_or(0);
let bcc: Option<Vec<String>> = message.bcc().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let cc: Option<Vec<String>> = message.cc().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let to: Option<Vec<String>> = message.to().map(|addr| {
AddrVec::from(addr)
.0
.into_iter()
.filter_map(|a| a.address)
.collect()
});
let from = message
.from()
.and_then(|addr| AddrVec::from(addr).0.into_iter().next())
.and_then(|add| add.address)
.unwrap_or_else(|| "unknown".to_string());
let attachments: Vec<String> = message
.attachments()
.filter_map(|att| att.attachment_name())
.map(|name| name.to_string())
.collect();
let envelope = Envelope {
id: create_hash(account_id, &message_id),
message_id,
account_id,
mailbox_id,
uid,
subject,
text,
from,
to: to.unwrap_or_default(),
cc: cc.unwrap_or_default(),
bcc: bcc.unwrap_or_default(),
date,
internal_date: date,
size,
thread_id,
attachments,
tags: None,
};
Ok(envelope)
}
pub fn compute_thread_id(
in_reply_to: Option<String>,
references: Option<Vec<String>>,
+2 -2
View File
@@ -144,7 +144,7 @@ impl Client {
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.ok_or_else(|| {
raise_error!(
"failed to read greeting".into(),
"Failed to read IMAP greeting — this usually indicates an incorrect encryption setting (SSL vs. STARTTLS). Your current setting is SSL.".into(),
ErrorCode::ImapCommandFailed
)
})?;
@@ -205,7 +205,7 @@ impl Client {
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?
.ok_or_else(|| {
raise_error!(
"failed to read greeting".into(),
"Failed to read IMAP greeting — this usually indicates an incorrect encryption setting (SSL vs. STARTTLS). Your current setting is STARTTLS.".into(),
ErrorCode::ImapCommandFailed
)
})?;
+162
View File
@@ -0,0 +1,162 @@
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use tantivy::doc;
use crate::{
base64_decode_url_safe,
modules::{
account::migration::{AccountModel, AccountType},
cache::imap::mailbox::{Attribute, AttributeEnum, MailBox},
envelope::extractor::extract_envelope_from_eml,
error::{code::ErrorCode, BichonResult},
indexer::{
manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
schema::SchemaTools,
},
utils::create_hash,
},
raise_error,
};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct BatchEmlRequest {
pub account_id: u64,
pub mail_folder: String,
/// A list of emails in base64-encoded format. Each element represents one .eml file.
pub emls: Vec<String>,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct FailedEmlDetail {
/// The 0-based index of the failed EML in the request list
pub index: usize,
/// The error message that caused the import to fail
pub error_message: String,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct BatchEmlResult {
/// Total number of emails processed
pub total: usize,
/// Number of emails successfully imported
pub success: usize,
/// Number of emails failed to import
pub failed: usize,
/// A list of details for failed imports
pub failed_details: Vec<FailedEmlDetail>,
}
pub struct ImportEmls;
impl ImportEmls {
pub async fn do_import(request: BatchEmlRequest) -> BichonResult<BatchEmlResult> {
let account = AccountModel::check_account_exists(request.account_id).await?;
if !account.enabled {
return Err(raise_error!("The account is disabled and cannot be used for this operation.".into(), ErrorCode::InvalidParameter));
}
let mailbox_id = match account.account_type {
AccountType::IMAP => {
let all_mailboxes = MailBox::list_all(account.id).await?;
let mailbox = all_mailboxes.into_iter().find(|m| m.name == request.mail_folder);
match mailbox {
Some(mailbox) => mailbox.id,
None => return Err(raise_error!(
format!("Mail folder '{}' not found for account ID {}. The target folder must exist before importing.",
request.mail_folder,
request.account_id).into(),
ErrorCode::ResourceNotFound
)),
}
},
AccountType::NoSync => {
let mailbox = MailBox {
id: create_hash(request.account_id, &request.mail_folder),
account_id: request.account_id,
name: request.mail_folder.clone(),
delimiter: Some("/".to_string()),
attributes: vec![Attribute {
attr: AttributeEnum::Extension,
extension: Some("CreatedByBichon".into()),
}],
exists: 0,
unseen: None,
uid_next: None,
uid_validity: None,
};
let mailbox_id = mailbox.id;
// Upsert the mailbox, creating it if it doesn't exist
MailBox::batch_upsert(&[mailbox]).await?;
mailbox_id
},
};
let fields = SchemaTools::eml_fields();
let account_id = account.id;
let mut success_count = 0;
let mut failed_details: Vec<FailedEmlDetail> = Vec::new(); // Store failure details
let total = request.emls.len();
for (index, eml_base64) in request.emls.into_iter().enumerate() {
// 1. Decode Base64
let decoded = match base64_decode_url_safe!(eml_base64.as_bytes()) {
Ok(bytes) => bytes,
Err(e) => {
let error_msg =
format!("Failed to decode base64 EML at index {}: {:?}", index, e);
tracing::error!("{}", error_msg);
failed_details.push(FailedEmlDetail {
index,
error_message: error_msg,
});
continue;
}
};
let envelope = match extract_envelope_from_eml(&decoded, account_id, mailbox_id) {
Ok(env) => env,
Err(e) => {
let error_msg = format!(
"Failed to extract envelope from EML at index {}: {:?}",
index, e
);
tracing::error!("{}", error_msg);
failed_details.push(FailedEmlDetail {
index,
error_message: error_msg,
});
continue;
}
};
ENVELOPE_INDEX_MANAGER
.add_document(envelope.id, envelope.to_document(mailbox_id).unwrap())
.await;
EML_INDEX_MANAGER
.add_document(
envelope.id,
doc!(
fields.f_id => envelope.id,
fields.f_account_id => account_id,
fields.f_mailbox_id => mailbox_id,
fields.f_eml => decoded
),
)
.await;
success_count += 1;
}
let failed_count = failed_details.len();
Ok(BatchEmlResult {
total,
success: success_count,
failed: failed_count,
failed_details, // Return the list of failure details
})
}
}
+1 -1
View File
@@ -16,7 +16,6 @@
// 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/>.
pub mod account;
pub mod autoconfig;
pub mod cache;
@@ -27,6 +26,7 @@ pub mod database;
pub mod envelope;
pub mod error;
pub mod imap;
pub mod import;
pub mod indexer;
pub mod logger;
pub mod mailbox;
+49
View File
@@ -0,0 +1,49 @@
//
// 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/>.
use crate::modules::common::auth::ClientContext;
use crate::modules::import::BatchEmlResult;
use crate::modules::import::{BatchEmlRequest, ImportEmls};
use crate::modules::rest::api::ApiTags;
use crate::modules::rest::ApiResult;
use poem_openapi::payload::Json;
use poem_openapi::OpenApi;
pub struct ImportApi;
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Import")]
impl ImportApi {
/// Batch import one or more EML files into a specified account and mail folder.
///
/// This endpoint accepts a JSON payload containing:
/// - `account_id`: the target account to import emails into
/// - `mail_folder`: the mailbox/folder name
/// - `emls`: a list of base64-encoded .eml files
///
/// Returns a summary of the import result, including total processed, successful, and failed emails.
#[oai(path = "/import", method = "post", operation_id = "do_batch_import")]
async fn do_batch_import(
&self,
/// JSON payload with account info and EML files to import
payload: Json<BatchEmlRequest>,
context: ClientContext,
) -> ApiResult<Json<BatchEmlResult>> {
context.require_root()?;
Ok(Json(ImportEmls::do_import(payload.0).await?))
}
}
+5 -2
View File
@@ -16,7 +16,6 @@
// 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/>.
use access_token::AccessTokenApi;
use account::AccountApi;
use auto_config::AutoConfigApi;
@@ -26,11 +25,12 @@ use oauth2::OAuth2Api;
use poem_openapi::{OpenApiService, Tags};
use system::SystemApi;
use crate::bichon_version;
use crate::{bichon_version, modules::rest::api::import::ImportApi};
pub mod access_token;
pub mod account;
pub mod auto_config;
pub mod import;
pub mod mailbox;
pub mod message;
pub mod oauth2;
@@ -45,6 +45,7 @@ pub enum ApiTags {
OAuth2,
Message,
System,
Import,
}
type RustMailOpenApi = (
@@ -55,6 +56,7 @@ type RustMailOpenApi = (
MailBoxApi,
OAuth2Api,
MessageApi,
ImportApi,
);
pub fn create_openapi_service() -> OpenApiService<RustMailOpenApi, ()> {
@@ -67,6 +69,7 @@ pub fn create_openapi_service() -> OpenApiService<RustMailOpenApi, ()> {
MailBoxApi,
OAuth2Api,
MessageApi,
ImportApi,
),
"BichonApi",
bichon_version!(),
+5
View File
@@ -13,7 +13,11 @@
"knip": "knip"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^3.9.1",
"@mui/material": "^7.3.5",
"@mui/x-tree-view": "^8.19.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
@@ -37,6 +41,7 @@
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@react-spring/web": "^10.0.3",
"@tabler/icons-react": "^3.24.0",
"@tanstack/react-query": "^5.62.3",
"@tanstack/react-router": "^1.86.1",
+564 -17
View File
@@ -8,9 +8,21 @@ importers:
.:
dependencies:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled':
specifier: ^11.14.1
version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@hookform/resolvers':
specifier: ^3.9.1
version: 3.9.1(react-hook-form@7.54.0(react@18.3.1))
'@mui/material':
specifier: ^7.3.5
version: 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/x-tree-view':
specifier: ^8.19.0
version: 8.19.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-accordion':
specifier: ^1.2.2
version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -80,6 +92,9 @@ importers:
'@radix-ui/react-visually-hidden':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-spring/web':
specifier: ^10.0.3
version: 10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tabler/icons-react':
specifier: ^3.24.0
version: 3.24.0(react@18.3.1)
@@ -350,10 +365,6 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.26.0':
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -378,6 +389,70 @@ packages:
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
engines: {node: '>=6.9.0'}
'@base-ui-components/utils@0.1.2':
resolution: {integrity: sha512-aEitDGpMsYO2qnSpYOwZNykn9Rzn2ioyEVk2fyDRH7t+TIHVKpp9CeV7SPTq43M9mMSDxQ+7UeZJVkrj2dCVIQ==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/babel-plugin@11.13.5':
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
'@emotion/cache@11.14.0':
resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
'@emotion/hash@0.9.2':
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
'@emotion/is-prop-valid@1.4.0':
resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==}
'@emotion/memoize@0.9.0':
resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
'@emotion/react@11.14.0':
resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
peerDependencies:
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/serialize@1.3.3':
resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
'@emotion/sheet@1.4.0':
resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
'@emotion/styled@11.14.1':
resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==}
peerDependencies:
'@emotion/react': ^11.0.0-rc.0
'@types/react': '*'
react: '>=16.8.0'
peerDependenciesMeta:
'@types/react':
optional: true
'@emotion/unitless@0.10.0':
resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
'@emotion/use-insertion-effect-with-fallbacks@1.2.0':
resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
peerDependencies:
react: '>=16.8.0'
'@emotion/utils@1.4.2':
resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
'@emotion/weak-memoize@0.4.0':
resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
'@esbuild/aix-ppc64@0.23.1':
resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
engines: {node: '>=18'}
@@ -722,6 +797,9 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
@@ -775,6 +853,108 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@mui/core-downloads-tracker@7.3.5':
resolution: {integrity: sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==}
'@mui/material@7.3.5':
resolution: {integrity: sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/material-pigment-css': ^7.3.5
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/material-pigment-css':
optional: true
'@types/react':
optional: true
'@mui/private-theming@7.3.5':
resolution: {integrity: sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/styled-engine@7.3.5':
resolution: {integrity: sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/system@7.3.5':
resolution: {integrity: sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/types@7.4.8':
resolution: {integrity: sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@7.3.5':
resolution: {integrity: sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/x-internals@8.19.0':
resolution: {integrity: sha512-mMmiyJAN5fW27srXJjhXhXJa+w2xGO45rwcjws6OQc9rdXGdJqRXhBwJd+OT7J1xwSdFIIUhjZRTz1KAfCSGBg==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/x-tree-view@8.19.0':
resolution: {integrity: sha512-1JPIczqK5qdDmo4p8KEpR4XZKxDHkw5dBoY1o+FNUdKmOipklQvg0+XL5wY75pjwzADngZR/6RYW08bhkt+Z3Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.9.0
'@emotion/styled': ^11.8.1
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -879,6 +1059,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@radix-ui/number@1.1.0':
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
@@ -1648,6 +1831,33 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@react-spring/animated@10.0.3':
resolution: {integrity: sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/core@10.0.3':
resolution: {integrity: sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/rafz@10.0.3':
resolution: {integrity: sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==}
'@react-spring/shared@10.0.3':
resolution: {integrity: sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/types@10.0.3':
resolution: {integrity: sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==}
'@react-spring/web@10.0.3':
resolution: {integrity: sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@rollup/rollup-android-arm-eabi@4.26.0':
resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==}
cpu: [arm]
@@ -2020,14 +2230,25 @@ packages:
'@types/node@22.10.1':
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/prop-types@15.7.15':
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
'@types/react-dom@18.3.5':
resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
peerDependencies:
'@types/react': ^18.0.0
'@types/react-transition-group@4.4.12':
resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==}
peerDependencies:
'@types/react': '*'
'@types/react@18.3.18':
resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
@@ -2243,6 +2464,10 @@ packages:
babel-dead-code-elimination@1.0.6:
resolution: {integrity: sha512-JxFi9qyRJpN0LjEbbjbN8g0ux71Qppn9R8Qe3k6QzHg2CaKsbUQtbn307LQGiDLGjV6JCtEFqfxzVig9MyDCHQ==}
babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -2358,9 +2583,16 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
cross-spawn@7.0.5:
resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==}
engines: {node: '>= 8'}
@@ -2491,6 +2723,9 @@ packages:
resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==}
engines: {node: '>=10.13.0'}
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -2624,6 +2859,9 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2731,6 +2969,9 @@ packages:
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
@@ -2780,6 +3021,9 @@ packages:
is-alphanumerical@2.0.1:
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@@ -3166,6 +3410,10 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
@@ -3185,6 +3433,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3377,6 +3629,9 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-is@19.2.0:
resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==}
react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies:
@@ -3456,9 +3711,6 @@ packages:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -3475,6 +3727,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3592,6 +3847,9 @@ packages:
style-to-object@1.0.8:
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -3891,6 +4149,10 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
yaml@2.6.0:
resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==}
engines: {node: '>= 14'}
@@ -4049,10 +4311,6 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/runtime@7.26.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.25.9':
@@ -4098,6 +4356,100 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@base-ui-components/utils@0.1.2(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@floating-ui/utils': 0.2.10
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
'@emotion/babel-plugin@11.13.5':
dependencies:
'@babel/helper-module-imports': 7.25.9
'@babel/runtime': 7.28.4
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/serialize': 1.3.3
babel-plugin-macros: 3.1.0
convert-source-map: 1.9.0
escape-string-regexp: 4.0.0
find-root: 1.1.0
source-map: 0.5.7
stylis: 4.2.0
transitivePeerDependencies:
- supports-color
'@emotion/cache@11.14.0':
dependencies:
'@emotion/memoize': 0.9.0
'@emotion/sheet': 1.4.0
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
stylis: 4.2.0
'@emotion/hash@0.9.2': {}
'@emotion/is-prop-valid@1.4.0':
dependencies:
'@emotion/memoize': 0.9.0
'@emotion/memoize@0.9.0': {}
'@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
hoist-non-react-statics: 3.3.2
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
transitivePeerDependencies:
- supports-color
'@emotion/serialize@1.3.3':
dependencies:
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/unitless': 0.10.0
'@emotion/utils': 1.4.2
csstype: 3.1.3
'@emotion/sheet@1.4.0': {}
'@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.4.0
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1)
'@emotion/utils': 1.4.2
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
transitivePeerDependencies:
- supports-color
'@emotion/unitless@0.10.0': {}
'@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)':
dependencies:
react: 18.3.1
'@emotion/utils@1.4.2': {}
'@emotion/weak-memoize@0.4.0': {}
'@esbuild/aix-ppc64@0.23.1':
optional: true
@@ -4303,6 +4655,8 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.8': {}
'@hookform/resolvers@3.9.1(react-hook-form@7.54.0(react@18.3.1))':
@@ -4354,6 +4708,115 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@mui/core-downloads-tracker@7.3.5': {}
'@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/core-downloads-tracker': 7.3.5
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/types': 7.4.8(@types/react@18.3.18)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@18.3.18)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 19.2.0
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
'@mui/private-theming@7.3.5(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
'@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/private-theming': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(react@18.3.1)
'@mui/types': 7.4.8(@types/react@18.3.18)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
'@mui/types@7.4.8(@types/react@18.3.18)':
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
'@types/react': 18.3.18
'@mui/utils@7.3.5(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/types': 7.4.8(@types/react@18.3.18)
'@types/prop-types': 15.7.15
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-is: 19.2.0
optionalDependencies:
'@types/react': 18.3.18
'@mui/x-internals@8.19.0(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
react: 18.3.1
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
'@mui/x-tree-view@8.19.0(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.4
'@base-ui-components/utils': 0.1.2(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
'@mui/utils': 7.3.5(@types/react@18.3.18)(react@18.3.1)
'@mui/x-internals': 8.19.0(@types/react@18.3.18)(react@18.3.1)
'@types/react-transition-group': 4.4.12(@types/react@18.3.18)
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@18.3.18)(react@18.3.1)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4430,6 +4893,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@popperjs/core@2.11.8': {}
'@radix-ui/number@1.1.0': {}
'@radix-ui/primitive@1.1.0': {}
@@ -5201,6 +5666,38 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@react-spring/animated@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/core@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/animated': 10.0.3(react@18.3.1)
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/rafz@10.0.3': {}
'@react-spring/shared@10.0.3(react@18.3.1)':
dependencies:
'@react-spring/rafz': 10.0.3
'@react-spring/types': 10.0.3
react: 18.3.1
'@react-spring/types@10.0.3': {}
'@react-spring/web@10.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@react-spring/animated': 10.0.3(react@18.3.1)
'@react-spring/core': 10.0.3(react@18.3.1)
'@react-spring/shared': 10.0.3(react@18.3.1)
'@react-spring/types': 10.0.3
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@rollup/rollup-android-arm-eabi@4.26.0':
optional: true
@@ -5526,12 +6023,20 @@ snapshots:
dependencies:
undici-types: 6.20.0
'@types/parse-json@4.0.2': {}
'@types/prop-types@15.7.14': {}
'@types/prop-types@15.7.15': {}
'@types/react-dom@18.3.5(@types/react@18.3.18)':
dependencies:
'@types/react': 18.3.18
'@types/react-transition-group@4.4.12(@types/react@18.3.18)':
dependencies:
'@types/react': 18.3.18
'@types/react@18.3.18':
dependencies:
'@types/prop-types': 15.7.14
@@ -5822,6 +6327,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.28.4
cosmiconfig: 7.1.0
resolve: 1.22.8
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -5934,8 +6445,18 @@ snapshots:
concat-map@0.0.1: {}
convert-source-map@1.9.0: {}
convert-source-map@2.0.0: {}
cosmiconfig@7.1.0:
dependencies:
'@types/parse-json': 4.0.2
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
cross-spawn@7.0.5:
dependencies:
path-key: 3.1.1
@@ -6024,7 +6545,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.26.0
'@babel/runtime': 7.28.4
csstype: 3.1.3
eastasianwidth@0.2.0: {}
@@ -6046,6 +6567,10 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.1
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
es-module-lexer@1.7.0:
optional: true
@@ -6237,6 +6762,8 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
find-root@1.1.0: {}
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -6350,6 +6877,10 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
@@ -6391,6 +6922,8 @@ snapshots:
is-alphabetical: 2.0.1
is-decimal: 2.0.1
is-arrayish@0.2.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
@@ -6450,8 +6983,7 @@ snapshots:
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1:
optional: true
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
@@ -6965,6 +7497,13 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.26.2
error-ex: 1.3.4
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-ms@4.0.0: {}
path-exists@4.0.0: {}
@@ -6978,6 +7517,8 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
path-type@4.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -7098,6 +7639,8 @@ snapshots:
react-is@18.3.1: {}
react-is@19.2.0: {}
react-markdown@10.1.0(@types/react@18.3.18)(react@18.3.1):
dependencies:
'@types/hast': 3.0.4
@@ -7159,7 +7702,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
'@babel/runtime': 7.28.4
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -7198,8 +7741,6 @@ snapshots:
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
regenerator-runtime@0.14.1: {}
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -7237,6 +7778,8 @@ snapshots:
require-from-string@2.0.2:
optional: true
reselect@5.1.1: {}
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -7371,6 +7914,8 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
stylis@4.2.0: {}
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.5
@@ -7701,6 +8246,8 @@ snapshots:
yallist@3.1.1: {}
yaml@1.10.2: {}
yaml@2.6.0: {}
yocto-queue@0.1.0: {}
+2
View File
@@ -54,6 +54,8 @@ export interface DashboardStats {
with_attachment_count: number; // Emails with attachments
without_attachment_count: number; // Emails without attachments
top_largest_emails: LargestEmail[]; // Top 10 largest emails
system_version: string, //The semantic version string of the currently running backend service
commit_hash: string //Git commit hash used to build this system version
}
export interface TimeBucket {
+2 -2
View File
@@ -82,7 +82,7 @@ export function EnvelopeListPagination({
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side='top'>
{[10, 20, 30, 40, 50].map((size) => (
{[10, 20, 30, 40, 50, 100].map((size) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
@@ -91,7 +91,7 @@ export function EnvelopeListPagination({
</Select>
</div>
<div className='flex items-center justify-center text-sm font-medium'>
{t("table.page")} {pageIndex + 1} {t("table.of")} {pageCount}
{t("table.page")} {pageIndex + 1} {t("table.of")} {pageCount}
</div>
<div className='flex items-center space-x-2'>
<Button
-529
View File
@@ -1,529 +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/>.
import React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronRight } from 'lucide-react'
import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const treeVariants = cva(
'group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 px-2 before:w-full before:opacity-0 before:bg-accent/70 before:h-[2rem] before:-z-10'
)
const selectedTreeVariants = cva(
'before:opacity-100 before:bg-accent/70 text-accent-foreground'
)
interface TreeDataItem {
id: string
name: string
icon?: React.ComponentType<{ className?: string }>
openIcon?: React.ComponentType<{ className?: string }>
children?: TreeDataItem[]
badge?: React.ReactNode,
attributes?: React.ReactNode,
onClick?: () => void
}
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
data: TreeDataItem[] | TreeDataItem
onSelectChange?: (item: TreeDataItem | undefined) => void
onSelectItemsChange?: (items: TreeDataItem[]) => void
expandAll?: boolean
multiple?: boolean
clickRowToSelect?: boolean
initialSelectedItemIds?: string[]
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
(
{
data,
onSelectChange,
onSelectItemsChange,
expandAll,
defaultLeafIcon,
defaultNodeIcon,
clickRowToSelect = true,
className,
multiple,
initialSelectedItemIds = [],
...props
},
ref
) => {
const [selectedItemIds, setSelectedItemIds] = React.useState<Set<string>>(
new Set(initialSelectedItemIds)
);
const callbacksRef = React.useRef({
onSelectChange,
onSelectItemsChange
});
React.useEffect(() => {
callbacksRef.current = {
onSelectChange,
onSelectItemsChange
};
}, [onSelectChange, onSelectItemsChange]);
const handleSelectChange = React.useCallback(
(item: TreeDataItem | undefined) => {
if (!item) return;
setSelectedItemIds(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
if (!multiple) {
newSet.clear();
}
newSet.add(item.id);
}
setTimeout(() => {
if (callbacksRef.current.onSelectChange) {
callbacksRef.current.onSelectChange(newSet.has(item.id) ? item : undefined);
}
if (callbacksRef.current.onSelectItemsChange) {
const selectedItems = Array.from(newSet)
.map(id => findItemById(data, id))
.filter(Boolean) as TreeDataItem[];
callbacksRef.current.onSelectItemsChange(selectedItems);
}
}, 0);
return newSet;
});
},
[multiple, onSelectChange, onSelectItemsChange, data]
);
const expandedItemIds = React.useMemo(() => {
if (!initialSelectedItemIds || initialSelectedItemIds.length === 0) {
return [] as string[]
}
const ids: string[] = []
function walkTreeItems(
items: TreeDataItem[] | TreeDataItem,
targetIds: string[]
) {
if (Array.isArray(items)) {
for (let i = 0; i < items.length; i++) {
ids.push(items[i]!.id)
if (walkTreeItems(items[i]!, targetIds) && !expandAll) {
return true
}
if (!expandAll) ids.pop()
}
} else if (!expandAll && targetIds.includes(items.id)) {
return true
} else if (items.children) {
return walkTreeItems(items.children, targetIds)
}
}
walkTreeItems(data, initialSelectedItemIds)
return ids
}, [data, expandAll, initialSelectedItemIds])
return (
<div className={cn('overflow-hidden relative p-2', className)}>
<TreeItem
data={data}
ref={ref}
clickRowToSelect={clickRowToSelect}
selectedItemIds={selectedItemIds}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
defaultLeafIcon={defaultLeafIcon}
defaultNodeIcon={defaultNodeIcon}
{...props}
/>
</div>
)
}
)
TreeView.displayName = 'TreeView'
// Helper function to find item by ID in tree
function findItemById(items: TreeDataItem[] | TreeDataItem, id: string): TreeDataItem | undefined {
if (Array.isArray(items)) {
for (const item of items) {
const found = findItemById(item, id);
if (found) return found;
}
} else {
if (items.id === id) return items;
if (items.children) {
return findItemById(items.children, id);
}
}
return undefined;
}
type TreeItemProps = TreeProps & {
selectedItemIds: Set<string>
handleSelectChange: (item: TreeDataItem | undefined) => void
expandedItemIds: string[]
clickRowToSelect?: boolean
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
(
{
className,
data,
selectedItemIds,
handleSelectChange,
clickRowToSelect,
expandedItemIds,
defaultNodeIcon,
defaultLeafIcon,
...props
},
ref
) => {
if (!Array.isArray(data)) {
data = [data]
}
return (
<div ref={ref} role="tree" className={className} {...props}>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.children ? (
<TreeNode
item={item}
selectedItemIds={selectedItemIds}
expandedItemIds={expandedItemIds}
clickRowToSelect={clickRowToSelect}
handleSelectChange={handleSelectChange}
defaultNodeIcon={defaultNodeIcon}
defaultLeafIcon={defaultLeafIcon}
/>
) : (
<TreeLeaf
item={item}
clickRowToSelect={clickRowToSelect}
selectedItemIds={selectedItemIds}
handleSelectChange={handleSelectChange}
defaultLeafIcon={defaultLeafIcon}
/>
)}
</li>
))}
</ul>
</div>
)
}
)
TreeItem.displayName = 'TreeItem'
interface TreeNodeProps {
item: TreeDataItem
handleSelectChange: (item: TreeDataItem | undefined) => void
expandedItemIds: string[]
clickRowToSelect?: boolean
selectedItemIds: Set<string>
defaultNodeIcon?: React.ComponentType<{ className?: string }>
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeNode = ({
item,
handleSelectChange,
expandedItemIds,
clickRowToSelect,
selectedItemIds,
defaultNodeIcon,
defaultLeafIcon
}: TreeNodeProps) => {
const [value, setValue] = React.useState(
expandedItemIds.includes(item.id) ? [item.id] : []
)
const isSelected = selectedItemIds.has(item.id);
return (
<AccordionPrimitive.Root
type="multiple"
value={value}
onValueChange={(s) => setValue(s)}
>
<AccordionPrimitive.Item value={item.id}>
<AccordionTrigger
className={cn(
"flex items-center w-full py-2",
treeVariants(),
isSelected && selectedTreeVariants()
)}
onClick={(e) => {
e.stopPropagation();
if (clickRowToSelect) {
handleSelectChange(item);
}
item.onClick?.();
}}
>
<div className="flex items-center min-w-0 flex-shrink-0">
<TreeIcon
item={item}
isSelected={isSelected}
isOpen={value.includes(item.id)}
default={defaultNodeIcon}
onCheck={() => { handleSelectChange(item) }}
/>
<span className="ml-2 text-sm truncate">
{item.name}
</span>
</div>
{item.attributes && (
<span className="mx-auto text-sm text-muted-foreground whitespace-nowrap">
{item.attributes}
</span>
)}
{item.badge && (
<TreeBadge isSelected={isSelected}>
{item.badge}
</TreeBadge>
)}
</AccordionTrigger>
<AccordionContent className="ml-4 pl-1 border-l">
<TreeItem
data={item.children ? item.children : item}
selectedItemIds={selectedItemIds}
clickRowToSelect={clickRowToSelect}
handleSelectChange={handleSelectChange}
expandedItemIds={expandedItemIds}
defaultLeafIcon={defaultLeafIcon}
defaultNodeIcon={defaultNodeIcon}
/>
</AccordionContent>
</AccordionPrimitive.Item>
</AccordionPrimitive.Root>
)
}
// function hasSelectedChildrenRecursive(item: TreeDataItem, selectedItemIds: Set<string>): boolean {
// if (!item.children) return false;
// return item.children.some(child =>
// selectedItemIds.has(child.id) ||
// (child.children && hasSelectedChildrenRecursive(child, selectedItemIds)));
// }
interface TreeLeafProps extends React.HTMLAttributes<HTMLDivElement> {
item: TreeDataItem
selectedItemIds: Set<string>
clickRowToSelect?: boolean
handleSelectChange: (item: TreeDataItem | undefined) => void
defaultLeafIcon?: React.ComponentType<{ className?: string }>
}
const TreeLeaf = React.forwardRef<HTMLDivElement, TreeLeafProps>(
(
{
className,
item,
clickRowToSelect,
selectedItemIds,
handleSelectChange,
defaultLeafIcon,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn(
"ml-5 flex items-center py-2 cursor-pointer before:right-1",
treeVariants(),
className,
selectedItemIds.has(item.id) && selectedTreeVariants()
)}
onClick={(e) => {
e.stopPropagation();
if (clickRowToSelect) {
handleSelectChange(item);
}
item.onClick?.();
}}
{...props}
>
<div className="flex items-center min-w-0 flex-shrink-0">
<TreeIcon
item={item}
isSelected={selectedItemIds.has(item.id)}
default={defaultLeafIcon}
onCheck={() => { handleSelectChange(item) }}
/>
<span className="ml-2 text-sm truncate">
{item.name}
</span>
</div>
{item.attributes && (
<span className="mx-auto text-sm text-muted-foreground whitespace-nowrap">
{item.attributes}
</span>
)}
{item.badge && (
<TreeBadge isSelected={selectedItemIds.has(item.id)}>
{item.badge}
</TreeBadge>
)}
</div>
)
}
)
TreeLeaf.displayName = 'TreeLeaf'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header>
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90',
className
)}
{...props}
onClick={(e) => {
e.stopPropagation()
if (props.onClick) {
props.onClick(e)
}
}}
>
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-accent-foreground/50 mr-1" />
{children}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
className
)}
{...props}
>
<div className="pb-1 pt-0">{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
interface TreeIconProps {
item: TreeDataItem;
isOpen?: boolean;
isSelected?: boolean;
default?: React.ComponentType<{ className?: string }>;
onCheck?: (checked: boolean) => void;
}
const TreeIcon = ({
item,
isOpen,
isSelected,
default: defaultIcon,
onCheck,
}: TreeIconProps) => {
let Icon = defaultIcon;
if (isOpen && item.openIcon) {
Icon = item.openIcon;
} else if (item.icon) {
Icon = item.icon;
}
return (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isSelected}
onChange={() => onCheck?.(!isSelected)}
onClick={(e) => e.stopPropagation()}
className={cn(
"h-4 w-4 rounded border border-primary dark:border-white shadow transition-all duration-200",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isSelected
? "bg-black dark:bg-white text-primary-foreground"
: "bg-transparent",
"appearance-none cursor-pointer flex items-center justify-center relative",
"after:content-[''] after:w-1.5 after:h-2",
"after:border-r-2 after:border-b-2 after:rotate-45 after:mt-[-2px]",
isSelected
? "after:block after:border-white dark:after:border-black after:z-10"
: "after:hidden"
)}
/>
{Icon && <Icon className="h-4 w-4 shrink-0" />}
</div>
);
};
interface TreeBadgeProps {
children: React.ReactNode
isSelected: boolean
showOnSelectedOnly?: boolean
}
const TreeBadge = ({
children,
isSelected,
showOnSelectedOnly = false
}: TreeBadgeProps) => {
return (
<div
className={cn(
showOnSelectedOnly
? isSelected
? 'block'
: 'hidden'
: 'block',
'absolute right-3 group-hover:block'
)}
>
{children}
</div>
)
}
export { TreeView, type TreeDataItem }
@@ -37,10 +37,10 @@ import { AccountModel } from '../data/schema';
import { useTranslation } from 'react-i18next';
const accountSchema = () =>
const accountSchema = (t: (key: string) => string) =>
z.object({
name: z.string().optional(),
email: z.string({ required_error: 'Email is required' }).email({ message: 'Invalid email address' }),
email: z.string({ required_error: t('validation.emailRequired') }).email({ message: t('validation.invalidEmail') }),
enabled: z.boolean()
});
@@ -85,7 +85,7 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const form = useForm<NoSyncAccount>({
mode: "all",
defaultValues: isEdit ? mapCurrentRowToFormValues(currentRow) : defaultValues,
resolver: zodResolver(accountSchema()),
resolver: zodResolver(accountSchema(t)),
});
const queryClient = useQueryClient();
@@ -118,7 +118,7 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const errorMessage =
(error.response?.data as { message?: string })?.message ||
error.message ||
`${isEdit ? 'Update' : 'Creation'} failed, please try again later`;
(isEdit ? t('accounts.updateFailed') : t('accounts.creationFailed'));
toast({
variant: "destructive",
@@ -134,7 +134,8 @@ export function NoSyncAccountDialog({ currentRow, open, onOpenChange }: Props) {
const commonData = {
email: data.email,
name: data.name,
enabled: data.enabled
enabled: data.enabled,
use_dangerous: false
};
if (isEdit) {
updateMutation.mutate(commonData);
@@ -33,7 +33,7 @@ export function OAuth2Action({ row }: DataTableRowActionsProps) {
const account_type = mailer.account_type;
if (account_type === "NoSync") {
return <span className="text-xs text-muted-foreground">n/a</span>
return <Button variant={"ghost"} className="text-xs text-muted-foreground">n/a</Button>
}
const isOAuth2 = mailer.imap?.auth.auth_type === "OAuth2"
@@ -26,20 +26,119 @@ import {
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Loader2, CheckSquare, Square } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { AccountModel } from '../data/schema'
import { toast } from '@/hooks/use-toast'
import { list_mailboxes } from '@/api/mailbox/api'
import { buildTree } from '@/lib/build-tree'
import { TreeDataItem, TreeView } from '@/components/tree-view'
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
import { Skeleton } from '@/components/ui/skeleton'
import { update_account } from '@/api/account/api'
import { ToastAction } from '@/components/ui/toast'
import { AxiosError } from 'axios'
import axios, { AxiosError } from 'axios'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { useTheme } from '@/context/theme-context'
import React from 'react'
import Collapse from '@mui/material/Collapse';
import { styled } from '@mui/material/styles';
import { TreeItemCheckbox, TreeItemContent, TreeItemIconContainer, TreeItemLabel, TreeItemRoot } from '@mui/x-tree-view/TreeItem'
import { TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemProvider, TreeViewBaseItem, TreeViewSelectionPropagation, useTreeItem, useTreeItemModel, UseTreeItemParameters } from '@mui/x-tree-view'
import { animated, useSpring } from '@react-spring/web';
import { TransitionProps } from '@mui/material/transitions'
function getParentIds(tree: TreeViewBaseItem[]): string[] {
const result: string[] = [];
function traverse(nodes: TreeViewBaseItem[]) {
for (const node of nodes) {
if (node.children && node.children.length > 0) {
result.push(node.id);
traverse(node.children);
}
}
}
traverse(tree);
return result;
}
interface CustomLabelProps {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
children: React.ReactNode;
icon?: React.ElementType;
expandable?: boolean;
}
function CustomLabel({
expandable,
exists,
attributes,
children,
...other
}: CustomLabelProps) {
return (
<TreeItemLabel
{...other}
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<span className="font-medium text-sm text-inherit">
{children}
</span>
<div className="flex gap-2 ml-auto mr-3 opacity-70 text-xs">
{attributes?.map((attr) => {
const text =
attr.attr === 'Extension'
? attr.extension
: attr.attr;
return (
<span key={attr.attr} className="text-inherit">
{text}
</span>
);
})}
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
>
{exists}
</span>
)}
</TreeItemLabel>
);
}
const CustomCollapse = styled(Collapse)({
padding: 0,
});
const AnimatedCollapse = animated(CustomCollapse);
function TransitionComponent(props: TransitionProps) {
const style = useSpring({
to: {
opacity: props.in ? 1 : 0,
transform: `translate3d(0,${props.in ? 0 : 20}px,0)`,
},
});
return <AnimatedCollapse style={style} {...props} />;
}
interface CustomTreeItemProps
extends Omit<UseTreeItemParameters, 'rootRef'>,
Omit<React.HTMLAttributes<HTMLLIElement>, 'onFocus'> { }
interface Props {
open: boolean
@@ -48,71 +147,151 @@ interface Props {
}
export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []);
const [selectedItems, setSelectedItems] = React.useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [allIds, setAllIds] = useState<string[]>([]);
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const [itemsWithChildren, setItemsWithChildren] = useState<string[]>([]);
const [mailboxes, setMailboxes] = useState<MailboxData[]>([]);
const [selectionPropagation, setSelectionPropagation] =
React.useState<TreeViewSelectionPropagation>({
parents: false,
descendants: true,
});
const [treeData, setTreeData] = useState<TreeViewBaseItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const queryClient = useQueryClient();
const { t } = useTranslation()
const { data: mailboxes, isLoading } = useQuery({
queryKey: ['account-mailboxes', currentRow.id],
queryFn: () => list_mailboxes(currentRow.id, true),
enabled: open,
});
const { theme } = useTheme()
// Convert mailbox names to IDs for initial selection
const initialSelectedItemIds = useMemo(() => {
if (!mailboxes) return [];
return mailboxes
.filter(mailbox => selectedFolders.includes(mailbox.name))
.map(mailbox => mailbox.id.toString());
}, [mailboxes, selectedFolders]);
useEffect(() => {
if (!open) return;
let cancelled = false;
const fetchMailboxes = async () => {
setIsLoading(true);
try {
const data = await list_mailboxes(currentRow.id, true);
if (!cancelled) {
setMailboxes(data);
const allIds = data.map(mailbox => String(mailbox.id));
setAllIds(allIds);
const treeData = useMemo(() => {
if (!mailboxes) return [];
return buildTree(mailboxes, undefined, true, true);
}, [mailboxes]);
const tree = buildTree(data);
setTreeData(tree);
const itemsWithChildren = getParentIds(tree);
setItemsWithChildren(itemsWithChildren);
setExpandedItems(itemsWithChildren);
const sync_folders = data
.filter(mailbox => currentRow.sync_folders.includes(mailbox.name))
.map(mailbox => mailbox.id.toString());
setSelectedItems(sync_folders);
setError(undefined);
}
} catch (err: any) {
if (axios.isAxiosError(err)) {
const resData = err.response?.data;
if (resData) {
setError(`Error ${resData.code || ''}: ${resData.message || ''}`);
} else {
setError(err.message);
}
}
} finally {
if (!cancelled) setIsLoading(false);
}
};
fetchMailboxes();
return () => {
cancelled = true;
};
}, [currentRow, open]);
const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => {
const allMailboxes = mailboxes || [];
const selected = selectedItems
.map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name)
.filter(Boolean) as string[];
const allMailSelected = selected.some(selectedName => {
const mailbox = allMailboxes.find(m => m.name === selectedName);
if (!mailbox) return false;
return mailbox.attributes.some(a => a.attr === 'All');
const handleExpandedItemsChange = (
_event: React.SyntheticEvent | null,
itemIds: string[],
) => {
setExpandedItems(itemIds);
};
const handleExpandClick = () => {
setExpandedItems((oldExpanded) =>
oldExpanded.length === 0 ? itemsWithChildren : [],
);
};
const CustomTreeItem = useMemo(() => {
return React.forwardRef(function CustomTreeItem(
props: CustomTreeItemProps,
ref: React.Ref<HTMLLIElement>,
) {
const { id, itemId, label, disabled, children, ...other } = props;
const {
getContextProviderProps,
getRootProps,
getContentProps,
getIconContainerProps,
getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
getDragAndDropOverlayProps,
status,
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
return (
<TreeItemProvider {...getContextProviderProps()}>
<TreeItemRoot {...getRootProps(other)}>
<TreeItemContent {...getContentProps()}>
<TreeItemIconContainer {...getIconContainerProps()}>
<TreeItemIcon status={status} />
</TreeItemIconContainer>
<TreeItemCheckbox {...getCheckboxProps()} />
<CustomLabel
{...getLabelProps({
exists: item.exists,
attributes: item.attributes,
expandable: status.expandable && status.expanded,
})}
/>
<TreeItemDragAndDropOverlay {...getDragAndDropOverlayProps()} />
</TreeItemContent>
{children && <TransitionComponent {...getGroupTransitionProps()} />}
</TreeItemRoot>
</TreeItemProvider>
);
});
}, [theme]);
if (allMailSelected) {
const handleSelectAll = useCallback(() => {
const selectedSet = new Set(allIds);
const selectedAllMailbox = mailboxes.find((mb) =>
selectedSet.has(String(mb.id)) &&
mb.attributes?.some(a => a.attr === 'All')
);
if (selectedAllMailbox) {
toast({
title: t('accounts.allMailFolderSelected'),
description: t('accounts.allMailFolderSelectedDesc'),
action: <ToastAction altText={t('common.ok')}>{t('common.ok')}</ToastAction>,
});
}
setSelectedFolders(selected);
}, [mailboxes]);
const handleSelectAll = useCallback(() => {
if (!mailboxes) return;
const validFolderNames = mailboxes
.filter(mailbox => {
const isAllMail = mailbox.attributes.some(a => a.attr === 'All');
if (isAllMail) return false;
return true;
})
.map(m => m.name);
setSelectedFolders(validFolderNames);
if (validFolderNames.length < mailboxes.length) {
toast({
description: t('accounts.allMailSkipped'),
});
}
}, [mailboxes]);
setSelectedItems(allIds);
}, [allIds]);
const handleDeselectAll = useCallback(() => {
setSelectedFolders([]);
setSelectedItems([]);
}, []);
@@ -149,8 +328,30 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
console.error(error);
}
const handleSelectedItemsChange = (
_event: React.SyntheticEvent | null,
newSelectedItems: string[],
) => {
const selectedSet = new Set(newSelectedItems);
const selectedAllMailbox = mailboxes.find((mb) =>
selectedSet.has(String(mb.id)) &&
mb.attributes?.some(a => a.attr === 'All')
);
if (selectedAllMailbox) {
toast({
title: t('accounts.allMailFolderSelected'),
description: t('accounts.allMailFolderSelectedDesc'),
action: <ToastAction altText={t('common.ok')}>{t('common.ok')}</ToastAction>,
});
}
setSelectedItems(newSelectedItems);
};
const handleSubmit = async () => {
if (selectedFolders.length === 0) {
if (selectedItems.length === 0) {
toast({
title: t('common.error'),
description: t('accounts.selectAtLeastOneFolder'),
@@ -159,14 +360,23 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
return;
}
setIsSubmitting(true);
const selectedNames: string[] = [];
const idSet = new Set(selectedItems);
for (const mailbox of mailboxes) {
if (idSet.has(String(mailbox.id))) {
selectedNames.push(mailbox.name);
}
}
updateMutation.mutate({
sync_folders: selectedFolders,
sync_folders: selectedNames,
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle>{t('accounts.selectSyncFolders')}</DialogTitle>
<DialogDescription>
@@ -175,13 +385,13 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between pt-2">
<div className="flex gap-2">
<div className="flex flex-col pt-2 gap-2">
<div className="flex gap-2 flex-wrap">
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
disabled={isLoading || !mailboxes || mailboxes.length === 0}
disabled={isLoading || !allIds || allIds.length === 0}
className="h-8"
>
<CheckSquare className="w-4 h-4 mr-2" />
@@ -191,23 +401,62 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
variant="outline"
size="sm"
onClick={handleDeselectAll}
disabled={isLoading || selectedFolders.length === 0}
disabled={isLoading || allIds.length === 0}
className="h-8"
>
<Square className="w-4 h-4 mr-2" />
{t('common.deselectAll')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setSelectionPropagation(prev => ({
...prev,
descendants: !prev.descendants,
}))
}
disabled={isLoading}
className="h-8"
>
{selectionPropagation.descendants ? <CheckSquare className="w-4 h-4 mr-2" /> : <Square className="w-4 h-4 mr-2" />}
{t('accounts.folderSync.autoSelectDescendants')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
setSelectionPropagation(prev => ({
...prev,
parents: !prev.parents,
}))
}
disabled={isLoading}
className="h-8"
>
{selectionPropagation.parents ? <CheckSquare className="w-4 h-4 mr-2" /> : <Square className="w-4 h-4 mr-2" />}
{t('accounts.folderSync.autoSelectParents')}
</Button>
<Button
variant="outline"
size="sm"
onClick={handleExpandClick}
disabled={isLoading}>
{expandedItems.length === 0 ? t('accounts.folderSync.expandAll') : t('accounts.folderSync.collapseAll')}
</Button>
</div>
<div className="text-sm text-muted-foreground">
{t('accounts.foldersSelected', { count: selectedFolders.length })}
{t('accounts.foldersSelected', { count: selectedItems.length })}
</div>
</div>
<ScrollArea className="h-[30rem] w-full pr-4 -mr-4 py-1">
<ScrollArea className="h-[35rem] w-full pr-4 -mr-4 py-1">
{isLoading && (
<div className="p-8 space-y-8">
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-sm font-medium">Loading mailbox folders</span>
<span className="text-sm font-medium">{t('accounts.folderSync.loadingMailboxFolders')}</span>
</div>
<div className="space-y-2">
@@ -218,16 +467,23 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
</div>
)}
{!isLoading && (
<TreeView
key={selectedFolders.length > 0 ? `tree-${selectedFolders.length}-${selectedFolders[0]}` : 'tree-empty'}
data={treeData}
multiple
expandAll
clickRowToSelect={false}
initialSelectedItemIds={initialSelectedItemIds}
onSelectItemsChange={handleSelectItems}
<RichTreeView
multiSelect
checkboxSelection
items={treeData}
expandedItems={expandedItems}
onExpandedItemsChange={handleExpandedItemsChange}
selectionPropagation={selectionPropagation}
selectedItems={selectedItems}
onSelectedItemsChange={handleSelectedItemsChange}
slots={{ item: CustomTreeItem }}
/>
)}
{error && (
<div className="mt-auto p-2 text-red-600 text-sm font-medium">
{error}
</div>
)}
</ScrollArea>
</div>
@@ -237,14 +493,14 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
{t('common.cancel')}
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || isLoading}
disabled={isSubmitting || isLoading || !!error}
>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
{t('common.save')}
</Button>
</DialogFooter>
</DialogContent>
+36 -8
View File
@@ -10,11 +10,11 @@
//
// 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
// 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/>.
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -22,7 +22,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
import { Mail, HardDrive, Database, Users, Inbox } from 'lucide-react';
import { Mail, HardDrive, Database, Users, Inbox, Info } from 'lucide-react';
import { formatBytes, formatNumber } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { get_dashboard_stats, TimeBucket } from '@/api/system/api';
@@ -94,7 +94,7 @@ export default function MailArchiveDashboard() {
queryFn: get_dashboard_stats,
});
const { t } = useTranslation();
const { t } = useTranslation();
const totalAttachments = (stats?.with_attachment_count ?? 0) + (stats?.without_attachment_count ?? 0);
const attachmentRatio = totalAttachments > 0 ? (stats?.with_attachment_count ?? 0) / totalAttachments : 0;
@@ -123,8 +123,8 @@ export default function MailArchiveDashboard() {
<Skeleton className="h-7 w-28 rounded-full" />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
{[...Array(5)].map((_, i) => (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-6">
{[...Array(6)].map((_, i) => (
<MetricCardSkeleton key={i} />
))}
</div>
@@ -156,8 +156,8 @@ export default function MailArchiveDashboard() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.mailAccounts')}</CardTitle>
@@ -212,6 +212,29 @@ export default function MailArchiveDashboard() {
<p className="text-xs text-muted-foreground">{t('dashboard.tantivyIndex')}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('dashboard.systemVersion')}</CardTitle>
<Info className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className='text-2xl font-bold'>
{stats!.system_version ? (
<a
href={`https://github.com/rustmailer/bichon/releases/tag/${stats!.system_version}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{stats!.system_version}
</a>
) : 'N/A'}
</div>
<p className="text-xs text-muted-foreground">
{stats!.commit_hash ?? 'N/A'}
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="trend" className="space-y-4">
@@ -406,6 +429,11 @@ export default function MailArchiveDashboard() {
</TabsContent>
</Tabs>
</div>
{/* Footer / Copyright - New Addition */}
<div className="p-6 md:p-8 pt-0 text-center text-xs text-muted-foreground">
© 2025 <a href="https://rustmailer.com" target="_blank" rel="noopener noreferrer" className="hover:underline">rustmailer.com</a> - Bichon Email Archiving Project
</div>
</Main>
</>
);
@@ -60,7 +60,6 @@ export function MailList({
}
}
const hasSelected = (mailId: number) => {
return selected.has(mailId);
}
@@ -77,15 +76,14 @@ export function MailList({
});
}
if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2.5">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-4 flex-1 max-w-xs" />
<Skeleton className="h-3 w-16 ml-auto" />
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 flex-1 max-w-xs" />
<Skeleton className="h-2.5 w-12 ml-auto" />
</div>
))}
</div>
@@ -95,7 +93,7 @@ export function MailList({
return (
<div className="divide-y divide-border">
{items.length > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-muted/30">
<div className="flex items-center gap-2 px-2 py-1 bg-muted/30">
<Checkbox
checked={
selected.size === items.length && items.length > 0
@@ -123,7 +121,7 @@ export function MailList({
<div
key={index}
className={cn(
"group flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors",
"group flex items-center gap-2 px-2 py-1.5 cursor-pointer transition-colors",
"hover:bg-accent/50",
isSelected && "bg-accent"
)}
@@ -140,12 +138,11 @@ export function MailList({
onClick={(e) => e.stopPropagation()}
className="h-4 w-4 shrink-0"
/>
<MailIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<MailIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-12 gap-1 sm:gap-0">
{/* LEFT AREA: From + Subject + Tags */}
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0 gap-1">
<div className="flex items-center gap-1 min-w-0">
<p className="text-sm font-medium truncate">{item.from}</p>
<h3 className="text-sm text-muted-foreground truncate hidden sm:block">
{item.subject}
@@ -156,7 +153,7 @@ export function MailList({
</h3>
{/* TAGS BELOW SUBJECT */}
<div className="flex flex-wrap gap-1 mt-0.5">
<div className="flex flex-wrap gap-1 mt-0.25">
{item.tags?.map((tag, i) => (
<Badge
key={i}
@@ -169,10 +166,10 @@ export function MailList({
</div>
{/* RIGHT AREA actions & meta */}
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-2 text-xs text-muted-foreground">
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-1 text-xs text-muted-foreground">
{hasAttachments && (
<div className="flex items-center gap-1">
<Paperclip className="h-3.5 w-3.5" />
<Paperclip className="h-3 w-3" />
<span>{item.attachments?.length}</span>
</div>
)}
@@ -190,7 +187,7 @@ export function MailList({
}}
className="p-1 rounded hover:bg-destructive/10 hover:text-destructive transition-all"
>
<Trash2 className="h-3.5 w-3.5" />
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
@@ -200,4 +197,4 @@ export function MailList({
{totalSelected > 0 && <MailBulkActions />}
</div>
)
}
}
+165 -13
View File
@@ -27,11 +27,9 @@ import {
import { Separator } from "@/components/ui/separator"
import { TooltipProvider } from "@/components/ui/tooltip"
import { AccountSwitcher } from "./account-switcher"
import { TreeView } from "@/components/tree-view"
import { ScrollArea } from "@/components/ui/scroll-area"
import { list_mailboxes, MailboxData } from "@/api/mailbox/api"
import { useQuery } from "@tanstack/react-query"
import { buildTree } from "../../../lib/build-tree"
import { Skeleton } from "@/components/ui/skeleton"
import MailboxProvider, { MailboxDialogType } from "../context"
import useDialogState from "@/hooks/use-dialog-state"
@@ -44,6 +42,14 @@ import { EnvelopeDeleteDialog } from "./delete-dialog"
import Logo from '@/assets/logo.svg'
import { EmailEnvelope } from "@/api"
import { EnvelopeListPagination } from "@/components/pagination"
import { RichTreeView, TreeItemCheckbox, TreeItemContent, TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemIconContainer, TreeItemLabel, TreeItemProvider, TreeItemRoot, useTreeItem, useTreeItemModel, UseTreeItemParameters } from "@mui/x-tree-view"
import { buildTree, ExtendedTreeItemProps } from "@/lib/build-tree"
import { useTheme } from "@/context/theme-context"
import { styled } from "@mui/material/styles"
import { animated, useSpring } from "@react-spring/web"
import { TransitionProps } from "@mui/material/transitions"
import Collapse from "@mui/material/Collapse"
import { FolderIcon } from "lucide-react"
interface MailProps {
@@ -73,6 +79,83 @@ const useListMessages = ({ accountId, mailboxId, page, page_size }: ListMessages
};
interface CustomLabelProps {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
children: React.ReactNode;
icon?: React.ElementType;
expandable?: boolean;
}
function CustomLabel({
expandable,
exists,
attributes,
children,
...other
}: CustomLabelProps) {
return (
<TreeItemLabel
{...other}
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<FolderIcon className="mr-2"/>
<span className="font-medium text-sm text-inherit">
{children}
</span>
{/* <div className="flex gap-2 ml-auto mr-3 opacity-70 text-xs">
{attributes?.map((attr) => {
const text =
attr.attr === 'Extension'
? attr.extension
: attr.attr;
return (
<span key={attr.attr} className="text-inherit">
{text}
</span>
);
})}
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
>
{exists}
</span>
)} */}
</TreeItemLabel>
);
}
const CustomCollapse = styled(Collapse)({
padding: 0,
});
const AnimatedCollapse = animated(CustomCollapse);
function TransitionComponent(props: TransitionProps) {
const style = useSpring({
to: {
opacity: props.in ? 1 : 0,
transform: `translate3d(0,${props.in ? 0 : 20}px,0)`,
},
});
return <AnimatedCollapse style={style} {...props} />;
}
interface CustomTreeItemProps
extends Omit<UseTreeItemParameters, 'rootRef'>,
Omit<React.HTMLAttributes<HTMLLIElement>, 'onFocus'> { }
export function Mail({
defaultLayout = [20, 80],
defaultCollapsed = false,
@@ -88,6 +171,7 @@ export function Mail({
const [pageSize, setPageSize] = React.useState(30);
const [deleteIds, setDeleteIds] = React.useState<Set<number>>(() => new Set());
const [selected, setSelected] = React.useState<Set<number>>(() => new Set());
const { theme } = useTheme()
const { data: mailboxes, isLoading: isMailboxesLoading } = useQuery({
queryKey: ['account-mailboxes', `${selectedAccountId}`],
@@ -95,6 +179,9 @@ export function Mail({
enabled: !!selectedAccountId,
})
const tree = buildTree(mailboxes ?? []);
const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({
accountId: selectedAccountId,
mailboxId: selectedMailbox?.id,
@@ -126,6 +213,65 @@ export function Mail({
}
}, [isError, error]);
const handleItemSelectionToggle = (
_event: React.SyntheticEvent | null,
itemId: string,
isSelected: boolean,
) => {
if (isSelected) {
setSelectedMailbox(mailboxes?.find(m => String(m.id) === itemId))
setPage(0);
}
};
const CustomTreeItem = React.useMemo(() => {
return React.forwardRef(function CustomTreeItem(
props: CustomTreeItemProps,
ref: React.Ref<HTMLLIElement>,
) {
const { id, itemId, label, disabled, children, ...other } = props;
const {
getContextProviderProps,
getRootProps,
getContentProps,
getIconContainerProps,
getCheckboxProps,
getLabelProps,
getGroupTransitionProps,
getDragAndDropOverlayProps,
status,
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
return (
<TreeItemProvider {...getContextProviderProps()}>
<TreeItemRoot {...getRootProps(other)}>
<TreeItemContent {...getContentProps()}>
<TreeItemIconContainer {...getIconContainerProps()}>
<TreeItemIcon status={status} />
</TreeItemIconContainer>
<TreeItemCheckbox {...getCheckboxProps()} />
<CustomLabel
{...getLabelProps({
exists: item.exists,
attributes: item.attributes,
expandable: status.expandable && status.expanded,
})}
/>
<TreeItemDragAndDropOverlay {...getDragAndDropOverlayProps()} />
</TreeItemContent>
{children && <TransitionComponent {...getGroupTransitionProps()} />}
</TreeItemRoot>
</TreeItemProvider>
);
});
}, [theme]);
return (
<MailboxProvider value={{ open, setOpen, currentMailbox: selectedMailbox, selectedAccountId, setCurrentMailbox: setSelectedMailbox, currentEnvelope: selectedEvelope, setCurrentEnvelope: setSelectedEvelope, deleteIds, setDeleteIds, selected, setSelected }}>
<TooltipProvider delayDuration={0}>
@@ -184,17 +330,23 @@ export function Mail({
))}
</div>
) : (
<TreeView
data={buildTree(mailboxes ?? [])}
clickRowToSelect={true}
onSelectChange={(item) => {
if (item) {
setSelectedMailbox(mailboxes?.find(m => m.id === parseInt(item.id, 10)))
setPage(0);
} else {
setSelectedMailbox(undefined)
}
}}
// <TreeView
// data={buildTree(mailboxes ?? [])}
// clickRowToSelect={true}
// onSelectChange={(item) => {
// if (item) {
// setSelectedMailbox(mailboxes?.find(m => m.id === parseInt(item.id, 10)))
// setPage(0);
// } else {
// setSelectedMailbox(undefined)
// }
// }}
// />
<RichTreeView
//checkboxSelection
items={tree}
onItemSelectionToggle={handleItemSelectionToggle}
slots={{ item: CustomTreeItem }}
/>
)}
</ScrollArea>
+24 -31
View File
@@ -21,7 +21,7 @@ import { cn, formatBytes } from "@/lib/utils"
import { formatDistanceToNow } from "date-fns"
import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
import { Checkbox } from "@/components/ui/checkbox" // shadcn Checkbox
import { Checkbox } from "@/components/ui/checkbox"
import { EmailEnvelope } from "@/api"
import { useSearchContext } from "./context"
import { MailBulkActions } from "./bulk-actions"
@@ -62,6 +62,7 @@ export function MailList({
});
}
}
const toggleToDelete = (accountId: number, mailId: number) => {
setToDelete(prev => {
const next = new Map(prev);
@@ -115,11 +116,11 @@ export function MailList({
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 p-3">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-3 w-20" />
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-3 flex-1" />
<Skeleton className="h-2.5 w-16" />
</div>
))}
</div>
@@ -129,7 +130,7 @@ export function MailList({
return (
<div className="divide-y divide-border">
{items.length > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-muted/30">
<div className="flex items-center gap-2 px-2 py-1 bg-muted/30">
<Checkbox
checked={
totalSelected === items.length && items.length > 0
@@ -158,7 +159,7 @@ export function MailList({
<div
key={index}
className={cn(
"flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors",
"flex items-center gap-2 px-2 py-1.5 cursor-pointer transition-colors",
"hover:bg-accent/50",
isSelectedRow && "bg-accent"
)}
@@ -175,41 +176,35 @@ export function MailList({
className="h-4 w-4 shrink-0"
/>
<MailIcon className="h-4 w-4 text-muted-foreground shrink-0" />
<MailIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0 grid grid-cols-1 sm:grid-cols-12 gap-1 sm:gap-0">
{/* LEFT AREA: From + Subject + Tags */}
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0">
{/* from + subject (large screen side by side, small screen subject hidden) */}
<div className="flex items-center gap-2 min-w-0">
<div className="col-span-1 sm:col-span-8 flex flex-col min-w-0 gap-0.5">
<div className="flex items-center gap-1 min-w-0">
<p className="text-sm font-medium truncate">{item.from}</p>
{/* subject on large screens */}
<h3 className="text-sm text-muted-foreground truncate hidden sm:block">
{item.subject}
</h3>
</div>
{/* subject on small screens */}
<h3 className="text-sm text-muted-foreground truncate sm:hidden">
{item.subject}
</h3>
{/* TAGS (always below on small screen, inline on large screen) */}
<div className="flex flex-wrap gap-1 mt-0.5">
{item.tags?.map((tag, index) => (
<Badge className="px-1 py-0.5 text-[10px] h-auto leading-none" key={index}>{tag}</Badge>
<div className="flex flex-wrap gap-1 mt-0.25">
{item.tags?.map((tag, i) => (
<Badge className="px-1 py-0.5 text-[10px] h-auto leading-none" key={i}>{tag}</Badge>
))}
</div>
</div>
{/* RIGHT AREA actions & meta */}
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-2 text-xs text-muted-foreground">
<div className="col-span-1 sm:col-span-4 flex items-center justify-end gap-1 text-xs text-muted-foreground">
{hasAttachments && (
<div className="flex items-center gap-1">
<Paperclip className="h-3.5 w-3.5" />
<Paperclip className="h-3 w-3" />
<span>{item.attachments?.length}</span>
</div>
)}
@@ -225,14 +220,14 @@ export function MailList({
<Button
variant="ghost"
size="icon"
className="h-7 w-7 p-0 hover:bg-muted rounded-md"
className="h-6 w-6 p-0 hover:bg-muted rounded-md"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-3.5 w-3.5" />
<MoreVertical className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem
onClick={(e) => e.stopPropagation()}
onSelect={(e) => {
@@ -241,7 +236,7 @@ export function MailList({
setOpen("edit-tags");
}}
>
<TagIcon className="ml-2 h-4 w-4" />
<TagIcon className="ml-2 h-3.5 w-3.5" />
{t('search.editTag')}
</DropdownMenuItem>
@@ -253,7 +248,7 @@ export function MailList({
handleDelete(item);
}}
>
<Trash2 className="ml-2 h-4 w-4" />
<Trash2 className="ml-2 h-3.5 w-3.5" />
{t('common.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -263,9 +258,7 @@ export function MailList({
</div>
)
})}
{totalSelected > 0 && (
<MailBulkActions />
)}
{totalSelected > 0 && <MailBulkActions />}
</div>
)
}
}
+78 -88
View File
@@ -18,100 +18,90 @@
import { MailboxData } from "@/api/mailbox/api";
import { TreeDataItem } from "@/components/tree-view";
import { Badge } from '@/components/ui/badge'
import { FolderClosed, FolderOpen } from "lucide-react";
import React from 'react';
import { TreeViewBaseItem } from '@mui/x-tree-view/models';
type BadgeContentFunction = (item: MailboxData) => React.ReactNode;
export const buildTree = (data: MailboxData[], badgeContent?: BadgeContentFunction, showAttributes?: boolean, showExists?: boolean): TreeDataItem[] => {
const root: TreeDataItem = {
id: 'root',
name: 'Root',
icon: FolderClosed,
openIcon: FolderOpen,
children: [], // Ensure children is initialized as an array
};
export type ExtendedTreeItemProps = {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
id: string;
label: string;
};
const nodeMap: { [key: string]: TreeDataItem } = {};
data.sort((a, b) => a.name.localeCompare(b.name));
data.forEach((item) => {
const { id, name, delimiter, exists, attributes } = item;
const badge = showExists ? (badgeContent ? badgeContent(item) : React.createElement(Badge, {
className: 'text-[12px]',
variant: 'secondary',
}, exists)) : null;
const attributesNode = showAttributes
? attributes.map((item, index) =>
React.createElement(
Badge,
{
key: index,
className: 'text-[12px] mr-1 last:mr-0',
variant: 'secondary',
},
item.attr === 'Extension' ? item.extension || '' : item.attr
)
)
: null;
// const badge = badgeContent || React.createElement(Badge, {
// className: 'text-xs',
// }, exists);
// If there is no delimiter, add the item directly as a child of the root
if (!delimiter) {
root.children!.push({
id: id.toString(),
name,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: undefined, // Leaf node, so children is undefined
});
return;
export function buildTree(items: MailboxData[]): TreeViewBaseItem<ExtendedTreeItemProps>[] {
const nodeByName = new Map<string, TreeViewBaseItem<ExtendedTreeItemProps>>();
for (const mb of items) {
if (!mb.name) continue;
const delimiter = mb.delimiter ?? '/';
const parts = mb.name.split(delimiter);
let currentFullName = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
currentFullName = currentFullName ? `${currentFullName}${delimiter}${part}` : part;
if (!nodeByName.has(currentFullName)) {
nodeByName.set(currentFullName, {
id: currentFullName,
label: part,
exists: mb.exists,
attributes: mb.attributes,
children: [],
});
}
if (i === parts.length - 1) {
const node = nodeByName.get(currentFullName)!;
node.id = String(mb.id);
}
}
}
const roots: TreeViewBaseItem<ExtendedTreeItemProps>[] = [];
for (const [fullName, node] of nodeByName.entries()) {
const delim = mbDelimiterOrDefault(fullName, items);
const lastDelimIndex = fullName.lastIndexOf(delim);
if (lastDelimIndex === -1) {
roots.push(node);
continue;
}
// Split the name into parts based on the delimiter
const parts = name.split(delimiter); // dir/sub1/sub2
let currentParent = root;
const parentFullName = fullName.substring(0, lastDelimIndex);
const parentNode = nodeByName.get(parentFullName);
// Traverse or create nodes for each part of the path
parts.forEach((part, index) => {
const path = parts.slice(0, index + 1).join(delimiter);
// If the node already exists, set it as the current parent
if (nodeMap[path]) {
currentParent = nodeMap[path];
} else {
// Determine if this is a leaf node (last part of the path)
const isLeaf = index === parts.length - 1;
// Create a new node
const newNode: TreeDataItem = {
id: isLeaf ? id.toString() : path, // Use item.id for leaf nodes, path for non-leaf nodes
name: part,
icon: FolderClosed,
openIcon: FolderOpen,
badge,
attributes: showAttributes ? attributesNode : null,
children: isLeaf ? undefined : [], // Ensure children is initialized as an array for non-leaf nodes
};
// Ensure currentParent.children is initialized as an array
if (!currentParent.children) {
currentParent.children = [];
}
// Add the new node to the current parent's children
currentParent.children.push(newNode);
currentParent = newNode; // Update the current parent to the new node
nodeMap[path] = newNode; // Store the node in the map for quick lookup
if (parentNode) {
parentNode.children = parentNode.children ?? [];
if (!parentNode.children.includes(node)) {
parentNode.children.push(node);
}
});
});
} else {
roots.push(node);
}
}
// Return the children of the root as the final tree structure
return root.children!;
};
const sortNodes = (nodes: TreeViewBaseItem[]) => {
nodes.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
for (const n of nodes) {
if (n.children && n.children.length) sortNodes(n.children);
}
};
const uniqueRoots = Array.from(new Set(roots));
sortNodes(uniqueRoots);
return uniqueRoots;
}
function mbDelimiterOrDefault(fullName: string, items: MailboxData[]): string {
const mb = items.find(it => it.name === fullName || fullName.startsWith(it.name + (it.delimiter ?? '/')));
if (mb?.delimiter) return mb.delimiter;
const withDelim = items.find(it => it.delimiter);
if (withDelim?.delimiter) return withDelim.delimiter;
return '.';
}
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "لا يوجد نشاط حديث",
"noSendersData": "لا توجد بيانات للمرسلين",
"noLargeEmails": "لا توجد رسائل بريد إلكتروني كبيرة",
"noAccountData": "لا توجد بيانات للحساب"
"noAccountData": "لا توجد بيانات للحساب",
"systemVersion": "إصدار النظام"
},
"accounts": {
"title": "حسابات البريد الإلكتروني",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "تحديث حساب البريد الإلكتروني هنا. ",
"addNewEmailAccountHere": "إضافة حساب بريد إلكتروني جديد هنا. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "يستخدم هذا الحساب لأغراض التعريف فقط ولا يتطلب المزامنة مع خادم بريد إلكتروني. إنه يساعد في استيراد بيانات البريد الإلكتروني.",
"determinesWhetherThisAccountIsActiveNoSync": "يحدد ما إذا كان هذا الحساب نشطًا. إذا تم تعطيله، فلن يتمكن الحساب من استيراد البيانات أو إجراء استعلامات.",
"determinesWhetherThisAccountIsActiveNoSync": "يحدد ما إذا كان هذا الحساب نشطًا. إذا تم تعطيله، فلن يكون قادرًا على استيراد البيانات.",
"folderSync": {
"noData": "لا توجد بيانات",
"noFolders": "لا توجد مجلدات للمزامنة",
"batches": "دفعات"
"batches": "دفعات",
"autoSelectDescendants": "تحديد العناصر الفرعية تلقائيًا",
"autoSelectParents": "تحديد العناصر الأصلية تلقائيًا",
"expandAll": "توسيع الكل",
"collapseAll": "طي الكل",
"loadingMailboxFolders": "جارٍ تحميل مجلدات البريد…"
},
"viewDetails": "عرض التفاصيل",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Ingen nylig aktivitet",
"noSendersData": "Ingen afsenderdata",
"noLargeEmails": "Ingen store e-mails",
"noAccountData": "Ingen kontodata"
"noAccountData": "Ingen kontodata",
"systemVersion": "Systemversion"
},
"accounts": {
"title": "E-mailkonti",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Opdater e-mailkontoen her. ",
"addNewEmailAccountHere": "Tilføj ny e-mailkonto her. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Denne konto bruges kun til identifikationsformål og kræver ikke synkronisering med en e-mailserver. Den hjælper med import af e-mail-data.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer, om denne konto er aktiv. Hvis deaktiveret, vil kontoen ikke kunne importere data eller udføre søgninger.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer, om denne konto er aktiv. Hvis den er deaktiveret, kan data ikke importeres.",
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper at synkronisere",
"batches": "batcher"
"batches": "batcher",
"autoSelectDescendants": "Vælg efterkommere automatisk",
"autoSelectParents": "Vælg forældre automatisk",
"expandAll": "Udvid alle",
"collapseAll": "Skjul alle",
"loadingMailboxFolders": "Indlæser postkassemapper…"
},
"viewDetails": "vis detaljer",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Keine kürzliche Aktivität",
"noSendersData": "Keine Absenderdaten",
"noLargeEmails": "Keine großen E-Mails",
"noAccountData": "Keine Kontodaten"
"noAccountData": "Keine Kontodaten",
"systemVersion": "Systemversion"
},
"accounts": {
"title": "E-Mail-Konten",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Aktualisieren Sie das E-Mail-Konto hier. ",
"addNewEmailAccountHere": "Fügen Sie hier ein neues E-Mail-Konto hinzu. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Dieses Konto dient nur zur Identifizierung und erfordert keine Synchronisierung mit dem Mailserver. Es hilft beim Import von E-Mail-Daten.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestimmt, ob dieses Konto aktiv ist. Wenn es deaktiviert ist, kann das Konto keine Daten importieren oder Abfragen ausführen.",
"determinesWhetherThisAccountIsActiveNoSync": "Legt fest, ob dieses Konto aktiv ist. Wenn es deaktiviert ist, können keine Daten importiert werden.",
"folderSync": {
"noData": "Keine Daten",
"noFolders": "Keine Ordner zum Synchronisieren",
"batches": "Batches"
"batches": "Batches",
"autoSelectDescendants": "Unterelemente automatisch auswählen",
"autoSelectParents": "Elternelemente automatisch auswählen",
"expandAll": "Alle erweitern",
"collapseAll": "Alle reduzieren",
"loadingMailboxFolders": "Postfachordner werden geladen…"
},
"viewDetails": "Details anzeigen",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "No recent activity",
"noSendersData": "No senders data",
"noLargeEmails": "No large emails",
"noAccountData": "No account data"
"noAccountData": "No account data",
"systemVersion": "System Version"
},
"accounts": {
"title": "Email Accounts",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Update the email account here. ",
"addNewEmailAccountHere": "Add new email account here. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "This account is used for identification purposes only and does not require syncing with an email server. It helps with importing email data.",
"determinesWhetherThisAccountIsActiveNoSync": "Determines whether this account is active. If disabled, the account will not be able to import data or perform queries.",
"determinesWhetherThisAccountIsActiveNoSync": "Determines whether this account is active. If disabled, the account will not be able to import data.",
"folderSync": {
"noData": "No Data",
"noFolders": "No folders to sync",
"batches": "batches"
"batches": "batches",
"autoSelectDescendants": "Auto select descendants",
"autoSelectParents": "Auto select parents",
"expandAll": "Expand all",
"collapseAll": "Collapse all",
"loadingMailboxFolders": "Loading mailbox folders…"
},
"viewDetails": "view details",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Sin actividad reciente",
"noSendersData": "Sin datos de remitentes",
"noLargeEmails": "Sin correos grandes",
"noAccountData": "Sin datos de cuenta"
"noAccountData": "Sin datos de cuenta",
"systemVersion": "Versión del sistema"
},
"accounts": {
"title": "Cuentas de correo",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Actualiza la cuenta de correo electrónico aquí. ",
"addNewEmailAccountHere": "Añade una nueva cuenta de correo electrónico aquí. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Esta cuenta se utiliza solo para fines de identificación y no requiere sincronización con el servidor de correo. Ayuda a importar datos de correo electrónico.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina si esta cuenta está activa. Si está deshabilitada, la cuenta no podrá importar datos ni ejecutar consultas.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina si esta cuenta está activa. Si está desactivada, no podrá importar datos.",
"folderSync": {
"noData": "Sin datos",
"noFolders": "No hay carpetas para sincronizar",
"batches": "lotes"
"batches": "lotes",
"autoSelectDescendants": "Seleccionar automáticamente los descendientes",
"autoSelectParents": "Seleccionar automáticamente los padres",
"expandAll": "Expandir todo",
"collapseAll": "Contraer todo",
"loadingMailboxFolders": "Cargando carpetas del buzón…"
},
"viewDetails": "ver detalles",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Ei viimeaikaisia tapahtumia",
"noSendersData": "Ei lähettäjätietoja",
"noLargeEmails": "Ei suuria sähköposteja",
"noAccountData": "Ei tilitietoja"
"noAccountData": "Ei tilitietoja",
"systemVersion": "Järjestelmäversio"
},
"accounts": {
"title": "Sähköpostitilit",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Päivitä sähköpostitili täällä. ",
"addNewEmailAccountHere": "Lisää uusi sähköpostitili täällä. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Tätä tiliä käytetään vain tunnistamistarkoituksiin, eikä se vaadi synkronointia sähköpostipalvelimen kanssa. Auttaa sähköpostidatan tuomisessa.",
"determinesWhetherThisAccountIsActiveNoSync": "Määrittää, onko tämä tili aktiivinen. Jos poistettu käytöstä, tili ei voi tuoda tietoja tai suorittaa kyselyjä.",
"determinesWhetherThisAccountIsActiveNoSync": "Määrittää, onko tämä tili aktiivinen. Jos se on poistettu käytöstä, tietoja ei voi tuoda.",
"folderSync": {
"noData": "Ei tietoja",
"noFolders": "Ei synkronoitavia kansioita",
"batches": "erät"
"batches": "erät",
"autoSelectDescendants": "Valitse alisolmut automaattisesti",
"autoSelectParents": "Valitse yläsolmut automaattisesti",
"expandAll": "Laajenna kaikki",
"collapseAll": "Kutista kaikki",
"loadingMailboxFolders": "Ladataan postilaatikon kansioita…"
},
"viewDetails": "katso tiedot",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Aucune activité récente",
"noSendersData": "Aucune donnée d'expéditeurs",
"noLargeEmails": "Aucun gros e-mail",
"noAccountData": "Aucune donnée de compte"
"noAccountData": "Aucune donnée de compte",
"systemVersion": "Version du système"
},
"accounts": {
"title": "Comptes de Messagerie",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Mettez à jour le compte e-mail ici. ",
"addNewEmailAccountHere": "Ajoutez un nouveau compte e-mail ici. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Ce compte est utilisé uniquement à des fins d'identification et ne nécessite pas de synchronisation avec un serveur de messagerie. Il est utile pour l'importation de données d'e-mails.",
"determinesWhetherThisAccountIsActiveNoSync": "Détermine si ce compte est actif. S'il est désactivé, l'importation de données ou l'exécution de requêtes ne sera pas possible pour ce compte.",
"determinesWhetherThisAccountIsActiveNoSync": "Détermine si ce compte est actif. Sil est désactivé, il ne pourra pas importer de données.",
"folderSync": {
"noData": "Aucune Donnée",
"noFolders": "Aucun dossier à synchroniser",
"batches": "lots"
"batches": "lots",
"autoSelectDescendants": "Sélectionner automatiquement les descendants",
"autoSelectParents": "Sélectionner automatiquement les parents",
"expandAll": "Tout développer",
"collapseAll": "Tout réduire",
"loadingMailboxFolders": "Chargement des dossiers de la boîte…"
},
"viewDetails": "voir les détails",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Nessuna attività recente",
"noSendersData": "Nessun dato sui mittenti",
"noLargeEmails": "Nessuna email di grandi dimensioni",
"noAccountData": "Nessun dato sull'account"
"noAccountData": "Nessun dato sull'account",
"systemVersion": "Versione del sistema"
},
"accounts": {
"title": "Account Email",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Aggiorna l'account email qui. ",
"addNewEmailAccountHere": "Aggiungi un nuovo account email qui. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Questo account viene utilizzato solo a scopo di identificazione e non richiede la sincronizzazione con un server email. Aiuta nell'importazione dei dati email.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se questo account è attivo. Se disabilitato, l'account non potrà importare dati o eseguire query.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se questo account è attivo. Se disattivato, non sarà possibile importare dati.",
"folderSync": {
"noData": "Nessun Dato",
"noFolders": "Nessuna cartella da sincronizzare",
"batches": "lotti"
"batches": "lotti",
"autoSelectDescendants": "Seleziona automaticamente i discendenti",
"autoSelectParents": "Seleziona automaticamente i genitori",
"expandAll": "Espandi tutto",
"collapseAll": "Comprimi tutto",
"loadingMailboxFolders": "Caricamento delle cartelle della casella…"
},
"viewDetails": "visualizza dettagli",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "最近のアクティビティなし",
"noSendersData": "送信者データなし",
"noLargeEmails": "サイズの大きいメールなし",
"noAccountData": "アカウントデータなし"
"noAccountData": "アカウントデータなし",
"systemVersion": "システムバージョン"
},
"accounts": {
"title": "メールアカウント",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "こちらでメールアカウントを更新してください。",
"addNewEmailAccountHere": "こちらで新しいメールアカウントを追加してください。",
"thisAccountIsUsedForIdentificationPurposesOnly": "このアカウントは識別目的でのみ使用され、メールサーバーとの同期は必要ありません。メールデータのインポートに役立ちます。",
"determinesWhetherThisAccountIsActiveNoSync": "このアカウントが有効かどうかを定します。無効の場合、データインポートやクエリの実行はできません。",
"determinesWhetherThisAccountIsActiveNoSync": "このアカウントが有効かどうかを定します。無効の場合、データインポートできません。",
"folderSync": {
"noData": "データなし",
"noFolders": "同期するフォルダーがありません",
"batches": "バッチ"
"batches": "バッチ",
"autoSelectDescendants": "子項目を自動選択",
"autoSelectParents": "親項目を自動選択",
"expandAll": "すべて展開",
"collapseAll": "すべて折りたたむ",
"loadingMailboxFolders": "メールボックスフォルダーを読み込み中…"
},
"viewDetails": "詳細を見る",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "최근 활동 없음",
"noSendersData": "발신자 데이터 없음",
"noLargeEmails": "크기가 큰 이메일 없음",
"noAccountData": "계정 데이터 없음"
"noAccountData": "계정 데이터 없음",
"systemVersion": "시스템 버전"
},
"accounts": {
"title": "이메일 계정",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "여기에서 이메일 계정을 업데이트하십시오.",
"addNewEmailAccountHere": "여기에서 새 이메일 계정을 추가하십시오.",
"thisAccountIsUsedForIdentificationPurposesOnly": "이 계정은 식별 목적으로만 사용되며 이메일 서버와의 동기화가 필요하지 않습니다. 이메일 데이터 가져오기에 도움이 됩니다.",
"determinesWhetherThisAccountIsActiveNoSync": "이 계정이 활성화되었는지 여부를 결정합니다. 비활성화된 경우 데이터 가져오기 또는 쿼리 실행이 불가능합니다.",
"determinesWhetherThisAccountIsActiveNoSync": "이 계정이 활성화되어 있는지 결정합니다. 비활성화되면 데이터 가져올 수 없습니다.",
"folderSync": {
"noData": "데이터 없음",
"noFolders": "동기화할 폴더가 없습니다",
"batches": "배치"
"batches": "배치",
"autoSelectDescendants": "하위 항목 자동 선택",
"autoSelectParents": "상위 항목 자동 선택",
"expandAll": "모두 펼치기",
"collapseAll": "모두 접기",
"loadingMailboxFolders": "메일 폴더 로딩 중…"
},
"viewDetails": "세부 정보 보기",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Geen recente activiteit",
"noSendersData": "Geen afzendersgegevens",
"noLargeEmails": "Geen grote e-mails",
"noAccountData": "Geen accountgegevens"
"noAccountData": "Geen accountgegevens",
"systemVersion": "Systeemversie"
},
"accounts": {
"title": "E-mailaccounts",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Werk het e-mailaccount hier bij. ",
"addNewEmailAccountHere": "Voeg hier een nieuw e-mailaccount toe. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Dit account wordt alleen gebruikt voor identificatiedoeleinden en vereist geen synchronisatie met een e-mailserver. Het helpt bij het importeren van e-mailgegevens.",
"determinesWhetherThisAccountIsActiveNoSync": "Bepaalt of dit account actief is. Indien uitgeschakeld, kan het account geen gegevens importeren of zoekopdrachten uitvoeren.",
"determinesWhetherThisAccountIsActiveNoSync": "Bepaalt of dit account actief is. Als het is uitgeschakeld, kan er geen data worden geïmporteerd.",
"folderSync": {
"noData": "Geen Gegevens",
"noFolders": "Geen mappen om te synchroniseren",
"batches": "batches"
"batches": "batches",
"autoSelectDescendants": "Automatisch onderliggende items selecteren",
"autoSelectParents": "Automatisch bovenliggende items selecteren",
"expandAll": "Alles uitklappen",
"collapseAll": "Alles inklappen",
"loadingMailboxFolders": "Postvakken laden…"
},
"viewDetails": "details bekijken",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Ingen nylig aktivitet",
"noSendersData": "Ingen avsenderdata",
"noLargeEmails": "Ingen store e-poster",
"noAccountData": "Ingen kontodata"
"noAccountData": "Ingen kontodata",
"systemVersion": "Systemversjon"
},
"accounts": {
"title": "E-postkontoer",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Oppdater e-postkontoen her. ",
"addNewEmailAccountHere": "Legg til ny e-postkonto her. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Denne kontoen brukes kun for identifikasjonsformål og krever ikke synkronisering med en e-postserver. Den hjelper med import av e-postdata.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer om denne kontoen er aktiv. Hvis deaktivert, vil ikke kontoen kunne importere data eller utføre spørringer.",
"determinesWhetherThisAccountIsActiveNoSync": "Bestemmer om denne kontoen er aktiv. Hvis den er deaktivert, kan data ikke importeres.",
"folderSync": {
"noData": "Ingen data",
"noFolders": "Ingen mapper å synkronisere",
"batches": "partier"
"batches": "partier",
"autoSelectDescendants": "Velg etterkommere automatisk",
"autoSelectParents": "Velg foreldre automatisk",
"expandAll": "Utvid alle",
"collapseAll": "Skjul alle",
"loadingMailboxFolders": "Laster inn postkassemapper…"
},
"viewDetails": "se detaljer",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Nenhuma Atividade Recente",
"noSendersData": "Sem Dados de Remetentes",
"noLargeEmails": "Sem Emails Grandes",
"noAccountData": "Sem Dados de Contas"
"noAccountData": "Sem Dados de Contas",
"systemVersion": "Versão do sistema"
},
"accounts": {
"title": "Contas de Email",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Atualize a conta de email aqui.",
"addNewEmailAccountHere": "Adicione uma nova conta de email aqui.",
"thisAccountIsUsedForIdentificationPurposesOnly": "Esta conta é usada apenas para fins de identificação e não requer sincronização com um servidor de email. Ajuda na importação de dados de email.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se esta conta está ativa. Se desativada, nenhuma importação de dados ou execução de consulta será possível.",
"determinesWhetherThisAccountIsActiveNoSync": "Determina se esta conta está ativa. Se estiver desativada, não será possível importar dados.",
"folderSync": {
"noData": "Sem Dados",
"noFolders": "Nenhuma pasta para sincronizar",
"batches": "Lotes"
"batches": "Lotes",
"autoSelectDescendants": "Selecionar automaticamente os descendentes",
"autoSelectParents": "Selecionar automaticamente os pais",
"expandAll": "Expandir tudo",
"collapseAll": "Recolher tudo",
"loadingMailboxFolders": "Carregando pastas da caixa de correio…"
},
"viewDetails": "Ver Detalhes",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Нет недавней активности",
"noSendersData": "Нет данных об отправителях",
"noLargeEmails": "Нет больших писем",
"noAccountData": "Нет данных об аккаунте"
"noAccountData": "Нет данных об аккаунте",
"systemVersion": "Версия системы"
},
"accounts": {
"title": "Почтовые учетные записи",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Обновите почтовый аккаунт здесь. ",
"addNewEmailAccountHere": "Добавьте новый почтовый аккаунт здесь. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Этот аккаунт используется только для идентификации и не требует синхронизации с почтовым сервером. Он помогает при импорте почтовых данных.",
"determinesWhetherThisAccountIsActiveNoSync": "Определяет, активен ли этот аккаунт. Если отключено, аккаунт не сможет импортировать данные или выполнять запросы.",
"determinesWhetherThisAccountIsActiveNoSync": "Определяет, активна ли учетная запись. Если она отключена, импорт данных будет невозможен.",
"folderSync": {
"noData": "Нет данных",
"noFolders": "Нет папок для синхронизации",
"batches": "пакетов"
"batches": "пакетов",
"autoSelectDescendants": "Автоматически выбирать дочерние элементы",
"autoSelectParents": "Автоматически выбирать родительские элементы",
"expandAll": "Развернуть все",
"collapseAll": "Свернуть все",
"loadingMailboxFolders": "Загрузка папок почтового ящика…"
},
"viewDetails": "смотреть детали",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "Ingen nylig aktivitet",
"noSendersData": "Inga avsändardata",
"noLargeEmails": "Inga stora e-postmeddelanden",
"noAccountData": "Inga kontodata"
"noAccountData": "Inga kontodata",
"systemVersion": "Systemversion"
},
"accounts": {
"title": "E-postkonton",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "Uppdatera e-postkontot här. ",
"addNewEmailAccountHere": "Lägg till nytt e-postkonto här. ",
"thisAccountIsUsedForIdentificationPurposesOnly": "Detta konto används endast för identifieringssyften och kräver inte synkronisering med en e-postserver. Det hjälper till med import av e-postdata.",
"determinesWhetherThisAccountIsActiveNoSync": "Avgör om detta konto är aktivt. Om inaktiverat kommer kontot inte kunna importera data eller utföra sökningar.",
"determinesWhetherThisAccountIsActiveNoSync": "Avgör om kontot är aktivt. Om det är inaktiverat kan ingen data importeras.",
"folderSync": {
"noData": "Inga data",
"noFolders": "Inga mappar att synkronisera",
"batches": "satser"
"batches": "satser",
"autoSelectDescendants": "Välj underordnade automatiskt",
"autoSelectParents": "Välj överordnade automatiskt",
"expandAll": "Expandera alla",
"collapseAll": "Komprimera alla",
"loadingMailboxFolders": "Laddar brevlådemappar…"
},
"viewDetails": "visa detaljer",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "無近期活動",
"noSendersData": "無寄件人資料",
"noLargeEmails": "無大容量郵件",
"noAccountData": "無帳號資料"
"noAccountData": "無帳號資料",
"systemVersion": "系統版本"
},
"accounts": {
"title": "郵件帳號",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "在此更新電子郵件帳號。",
"addNewEmailAccountHere": "在此新增電子郵件帳號。",
"thisAccountIsUsedForIdentificationPurposesOnly": "此帳號僅用於識別目的,不需要與郵件伺服器同步。有助於匯入郵件資料。",
"determinesWhetherThisAccountIsActiveNoSync": "決定此帳號是否啟用狀態。如果停用,無法匯入資料或執行查詢。",
"determinesWhetherThisAccountIsActiveNoSync": "用來判斷帳戶是否啟用。若停用,無法匯入資料。",
"folderSync": {
"noData": "無資料",
"noFolders": "沒有可同步的資料夾",
"batches": "批次"
"batches": "批次",
"autoSelectDescendants": "自動選取子項",
"autoSelectParents": "自動選取父項",
"expandAll": "全部展開",
"collapseAll": "全部收合",
"loadingMailboxFolders": "正在載入郵件資料夾…"
},
"viewDetails": "檢視詳細資訊",
"runningState": {
+9 -3
View File
@@ -126,7 +126,8 @@
"noRecentActivity": "无最近活动",
"noSendersData": "无发件人数据",
"noLargeEmails": "无大邮件",
"noAccountData": "无账户数据"
"noAccountData": "无账户数据",
"systemVersion": "系统版本"
},
"accounts": {
"title": "邮件账户",
@@ -250,11 +251,16 @@
"updateTheEmailAccountHere": "在此更新邮件账户。",
"addNewEmailAccountHere": "在此添加新邮件账户。",
"thisAccountIsUsedForIdentificationPurposesOnly": "此账户仅用于识别目的,不需要与邮件服务器同步。它有助于导入邮件数据。",
"determinesWhetherThisAccountIsActiveNoSync": "确定此账户是否处于活动状态。如果禁用,账户将无法导入数据或执行查询。",
"determinesWhetherThisAccountIsActiveNoSync": "用于判断账户是否处于启用状态。如果禁用,将无法导入数据。",
"folderSync": {
"noData": "无数据",
"noFolders": "没有需要同步的文件夹",
"batches": "批次"
"batches": "批次",
"autoSelectDescendants": "自动选择子项",
"autoSelectParents": "自动选择父项",
"expandAll": "全部展开",
"collapseAll": "全部收起",
"loadingMailboxFolders": "正在加载邮箱文件夹…"
},
"viewDetails": "查看详情",
"runningState": {