fix: upload failure, share dialog, shared view styling, dark mode fixes
- Fix 'folder_id is required' upload error by adding create_home_folder through the full hexagonal architecture (trait, service, repository, auth) - Fix double upload issue with _isUploading concurrency guard - Fix Share context menu doing nothing (ID collision between sharedView and main share dialog resolved with sv- prefix) - Fix Compartidos tab duplicate headers and broken layout - Add missing .shared-dialog CSS with dark mode support - Fix dark mode white backgrounds on empty-state, shared-filters, trash-actions, action-btn, and header - Fix i18n key mismatches in sharedView - Bump version to 0.4.1 Closes #120
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "oxicloud"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2024"
|
||||
default-run = "oxicloud"
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
||||
|
||||
/// Deletes a folder (ownership verified against caller_id)
|
||||
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Creates a root-level home folder for a user during registration.
|
||||
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<FolderDto, DomainError>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::application::dtos::folder_dto::CreateFolderDto;
|
||||
use crate::application::dtos::user_dto::{
|
||||
AuthResponseDto, ChangePasswordDto, LoginDto, RefreshTokenDto, RegisterDto, UserDto,
|
||||
};
|
||||
@@ -318,43 +317,7 @@ impl AuthApplicationService {
|
||||
let created_user = self.user_storage.create_user(user).await?;
|
||||
|
||||
// Create personal folder for the user
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for user {}: {} (ID: {})",
|
||||
created_user.id(),
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
|
||||
// Here we could save the folder-to-user association,
|
||||
// for example, in a folder-user relationship table
|
||||
}
|
||||
Err(e) => {
|
||||
// We don't fail registration due to a folder creation error,
|
||||
// but we log it for investigation
|
||||
tracing::error!(
|
||||
"Could not create personal folder for user {}: {}",
|
||||
created_user.id(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Folder service not configured, cannot create personal folder for user: {}",
|
||||
created_user.id()
|
||||
);
|
||||
}
|
||||
self.create_personal_folder(&dto.username, created_user.id()).await;
|
||||
|
||||
tracing::info!("User registered: {}", created_user.id());
|
||||
Ok(UserDto::from(created_user))
|
||||
@@ -695,34 +658,8 @@ impl AuthApplicationService {
|
||||
// 4. Save the new admin user
|
||||
let created_user = self.user_storage.create_user(user).await?;
|
||||
|
||||
// 5. Create personal folder for the new admin if folder service is available
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for admin {}: {} (ID: {})",
|
||||
created_user.id(),
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not create personal folder for admin {}: {}",
|
||||
created_user.id(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 5. Create personal folder for the new admin
|
||||
self.create_personal_folder(&dto.username, created_user.id()).await;
|
||||
|
||||
tracing::info!("Custom admin created: {}", created_user.id());
|
||||
Ok(UserDto::from(created_user))
|
||||
@@ -828,32 +765,7 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
// Create personal folder
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", dto.username);
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name,
|
||||
parent_id: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
tracing::info!(
|
||||
"Personal folder created for admin-created user {}: {} (ID: {})",
|
||||
created.id(),
|
||||
folder.name,
|
||||
folder.id
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Could not create personal folder for user {}: {}",
|
||||
created.id(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.create_personal_folder(&dto.username, created.id()).await;
|
||||
|
||||
tracing::info!("Admin created user: {} ({})", dto.username, created.id());
|
||||
Ok(UserDto::from(created))
|
||||
@@ -1290,10 +1202,7 @@ impl AuthApplicationService {
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", username);
|
||||
match folder_service
|
||||
.create_folder(CreateFolderDto {
|
||||
name: folder_name.clone(),
|
||||
parent_id: None,
|
||||
})
|
||||
.create_home_folder(user_id, folder_name.clone())
|
||||
.await
|
||||
{
|
||||
Ok(folder) => {
|
||||
|
||||
@@ -111,6 +111,10 @@ impl FolderService {
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_home_folder(&self, _user_id: &str, _name: String) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
}
|
||||
|
||||
FolderServiceStub
|
||||
@@ -154,6 +158,22 @@ impl FolderUseCase for FolderService {
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Creates a root-level home folder for a user during registration.
|
||||
async fn create_home_folder(&self, user_id: &str, name: String) -> Result<FolderDto, DomainError> {
|
||||
let folder = self
|
||||
.folder_storage
|
||||
.create_home_folder(user_id, name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to create home folder: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Gets a folder by its ID
|
||||
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
|
||||
@@ -609,6 +609,14 @@ mod tests {
|
||||
async fn delete_folder_permanently(&self, _folder_id: &str) -> Result<(), DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_name: String,
|
||||
) -> Result<crate::domain::entities::folder::Folder, DomainError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct MockShareRepository {
|
||||
|
||||
@@ -461,6 +461,14 @@ impl FolderRepository for MockFolderRepository {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_name: String,
|
||||
) -> std::result::Result<Folder, DomainError> {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -322,6 +322,14 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
async fn delete_folder_permanently(&self, _folder_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_name: String,
|
||||
) -> Result<Folder, DomainError> {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -414,6 +422,10 @@ impl FolderUseCase for StubFolderUseCase {
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_home_folder(&self, _user_id: &str, _name: String) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -99,4 +99,12 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
|
||||
/// Permanently deletes a folder (used by the trash)
|
||||
async fn delete_folder_permanently(&self, folder_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Creates a root-level home folder for a user.
|
||||
/// This is used during user registration to create the user's personal folder.
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
user_id: &str,
|
||||
name: String,
|
||||
) -> Result<Folder, DomainError>;
|
||||
}
|
||||
|
||||
@@ -603,17 +603,11 @@ impl FolderRepository for FolderDbRepository {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Extra helpers for blob-storage bootstrap ──
|
||||
|
||||
impl FolderDbRepository {
|
||||
/// Creates a root-level home folder for a user.
|
||||
/// This is called during user registration.
|
||||
pub async fn create_home_folder(
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
user_id: &str,
|
||||
name: &str,
|
||||
name: String,
|
||||
) -> Result<Folder, DomainError> {
|
||||
let row = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||
r#"
|
||||
@@ -626,14 +620,14 @@ impl FolderDbRepository {
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&name)
|
||||
.bind(user_id)
|
||||
.fetch_optional(self.pool())
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?;
|
||||
|
||||
match row {
|
||||
Some((id, path, ca, ma)) => Self::row_to_folder(id, name.to_string(), path, None, Some(user_id.to_string()), ca, ma),
|
||||
Some((id, path, ca, ma)) => Self::row_to_folder(id, name.clone(), path, None, Some(user_id.to_string()), ca, ma),
|
||||
None => {
|
||||
// Already exists — fetch it
|
||||
let existing = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||
@@ -646,16 +640,20 @@ impl FolderDbRepository {
|
||||
WHERE name = $1 AND user_id = $2 AND parent_id IS NULL
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(&name)
|
||||
.bind(user_id)
|
||||
.fetch_one(self.pool())
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("home fetch: {e}")))?;
|
||||
Self::row_to_folder(existing.0, name.to_string(), existing.1, None, Some(user_id.to_string()), existing.2, existing.3)
|
||||
Self::row_to_folder(existing.0, name, existing.1, None, Some(user_id.to_string()), existing.2, existing.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Extra helpers for blob-storage bootstrap ──
|
||||
|
||||
impl FolderDbRepository {
|
||||
/// Returns user_id for a given folder. Used by file repositories.
|
||||
pub async fn get_folder_user_id(&self, folder_id: &str) -> Result<String, DomainError> {
|
||||
sqlx::query_scalar::<_, String>("SELECT user_id FROM storage.folders WHERE id = $1::uuid")
|
||||
|
||||
+250
-1
@@ -2498,6 +2498,225 @@ select:focus {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Shared View Dialog (edit/notification dialogs inside sharedView component) */
|
||||
.shared-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.45);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
backdrop-filter: blur(2px);
|
||||
animation: modalFadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.shared-dialog.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.shared-dialog-content {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
width: 480px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
|
||||
overflow: hidden;
|
||||
animation: modalSlideIn 0.25s ease;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.shared-dialog-header {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shared-dialog-header .close-dialog-btn {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: #718096;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.shared-dialog-header .close-dialog-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.shared-dialog .share-link-section,
|
||||
.shared-dialog .share-permissions-section,
|
||||
.shared-dialog .share-password-section,
|
||||
.shared-dialog .share-expiration-section,
|
||||
.shared-dialog .notification-form {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.shared-dialog .share-link-section label,
|
||||
.shared-dialog .share-permissions-section h4,
|
||||
.shared-dialog .notification-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #2d3748;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shared-dialog .share-link-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shared-dialog .share-link-input input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
background: #f8fafc;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.shared-dialog .share-permissions-section label,
|
||||
.shared-dialog .share-password-section label,
|
||||
.shared-dialog .share-expiration-section label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.shared-dialog .password-input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.shared-dialog .password-input-group input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.shared-dialog .share-expiration-section input[type="date"] {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
background: #f8fafc;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.shared-dialog .share-actions,
|
||||
.shared-dialog .notification-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.shared-dialog .notification-form input,
|
||||
.shared-dialog .notification-form textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
background: #f8fafc;
|
||||
color: #1a202c;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.shared-dialog .notification-form input:focus,
|
||||
.shared-dialog .notification-form textarea:focus {
|
||||
border-color: #ff5e3a;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(255,94,58,0.1);
|
||||
}
|
||||
|
||||
.shared-dialog .notification-form .form-group {
|
||||
padding: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Dark mode for shared-dialog */
|
||||
[data-theme="dark"] .shared-dialog-content {
|
||||
background-color: #1e293b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog-header {
|
||||
color: #e2e8f0;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog-header .close-dialog-btn {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog-header .close-dialog-btn:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog .share-link-input input,
|
||||
[data-theme="dark"] .shared-dialog .password-input-group input,
|
||||
[data-theme="dark"] .shared-dialog .share-expiration-section input,
|
||||
[data-theme="dark"] .shared-dialog .notification-form input,
|
||||
[data-theme="dark"] .shared-dialog .notification-form textarea {
|
||||
background: #0f172a;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog .share-link-input input:focus,
|
||||
[data-theme="dark"] .shared-dialog .notification-form input:focus,
|
||||
[data-theme="dark"] .shared-dialog .notification-form textarea:focus {
|
||||
border-color: #ff5e3a;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog .share-link-section label,
|
||||
[data-theme="dark"] .shared-dialog .share-permissions-section h4,
|
||||
[data-theme="dark"] .shared-dialog .notification-form label {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog .share-permissions-section label,
|
||||
[data-theme="dark"] .shared-dialog .share-password-section label,
|
||||
[data-theme="dark"] .shared-dialog .share-expiration-section label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shared-dialog .share-actions,
|
||||
[data-theme="dark"] .shared-dialog .notification-actions {
|
||||
background: #162032;
|
||||
border-top-color: #334155;
|
||||
}
|
||||
|
||||
/* Folder select items in move dialog */
|
||||
.folder-select-item {
|
||||
display: flex;
|
||||
@@ -4535,6 +4754,8 @@ html[dir='rtl'] .fa-sign-out-alt {
|
||||
|
||||
/* Empty state */
|
||||
[data-theme="dark"] .empty-state {
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
color: #64748b;
|
||||
}
|
||||
[data-theme="dark"] .empty-state i {
|
||||
@@ -4633,7 +4854,8 @@ html[dir='rtl'] .fa-sign-out-alt {
|
||||
|
||||
/* ── Shared View Dark Mode ── */
|
||||
[data-theme="dark"] .shared-filters {
|
||||
background-color: transparent;
|
||||
background-color: #1e293b;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
[data-theme="dark"] .filter-group label {
|
||||
color: #94a3b8;
|
||||
@@ -4776,4 +4998,31 @@ html[dir='rtl'] .fa-sign-out-alt {
|
||||
}
|
||||
[data-theme="dark"] .search-sort-select:focus {
|
||||
border-color: #ff5e3a;
|
||||
}
|
||||
|
||||
/* Trash & action buttons dark mode */
|
||||
[data-theme="dark"] .trash-actions button,
|
||||
[data-theme="dark"] .actions-cell button {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
[data-theme="dark"] .trash-actions button:hover,
|
||||
[data-theme="dark"] .actions-cell button:hover {
|
||||
background: #334155;
|
||||
}
|
||||
[data-theme="dark"] .action-btn {
|
||||
background-color: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
[data-theme="dark"] .action-btn:hover {
|
||||
background-color: #334155;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
/* Header dark mode (shared page) */
|
||||
[data-theme="dark"] header {
|
||||
background-color: #1e293b;
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
+21
-2
@@ -543,6 +543,7 @@ function setupEventListeners() {
|
||||
|
||||
// Update UI
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.trash');
|
||||
elements.actionsBar.innerHTML = `
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-danger" id="empty-trash-btn">
|
||||
@@ -1189,17 +1190,28 @@ function switchToSharedView() {
|
||||
sharedNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
// Update UI
|
||||
// Update UI — also set data-i18n so translatePage() doesn't overwrite
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.shared');
|
||||
|
||||
// Clear breadcrumb and show root
|
||||
ui.updateBreadcrumb('');
|
||||
|
||||
// Hide breadcrumb itself
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
if (breadcrumb) breadcrumb.style.display = 'none';
|
||||
|
||||
// Hide standard actions bar
|
||||
if (elements.actionsBar) {
|
||||
elements.actionsBar.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide file containers
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
if (filesGrid) filesGrid.style.display = 'none';
|
||||
if (filesListView) filesListView.style.display = 'none';
|
||||
|
||||
// Init and show shared view
|
||||
if (window.sharedView) {
|
||||
window.sharedView.init();
|
||||
@@ -1218,8 +1230,13 @@ function switchToFilesView() {
|
||||
app.isRecentView = false;
|
||||
app.currentSection = 'files';
|
||||
|
||||
// Update UI
|
||||
// Update UI — restore data-i18n to files
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.files');
|
||||
|
||||
// Restore breadcrumb visibility
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
if (breadcrumb) breadcrumb.style.display = '';
|
||||
|
||||
// Remove active class from all nav items
|
||||
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
@@ -1334,6 +1351,7 @@ function switchToFavoritesView() {
|
||||
|
||||
// Update UI
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.favorites');
|
||||
|
||||
// Clear breadcrumb and show root
|
||||
ui.updateBreadcrumb('');
|
||||
@@ -1423,6 +1441,7 @@ function switchToRecentFilesView() {
|
||||
|
||||
// Update UI
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.recent');
|
||||
|
||||
// Clear breadcrumb and show root
|
||||
ui.updateBreadcrumb('');
|
||||
|
||||
@@ -68,20 +68,19 @@ const sharedView = {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `
|
||||
<div class="shared-header">
|
||||
<h2 data-i18n="nav.shared">Shared Files</h2>
|
||||
<div class="shared-filters">
|
||||
<select id="filter-type" class="shared-filter-select">
|
||||
<option value="all" data-i18n="shared_allTypes">All types</option>
|
||||
<option value="file" data-i18n="shared_files">Files</option>
|
||||
<option value="folder" data-i18n="shared_folders">Folders</option>
|
||||
<option value="all" data-i18n="shared_filterAll">All</option>
|
||||
<option value="file" data-i18n="shared_filterFiles">Files</option>
|
||||
<option value="folder" data-i18n="shared_filterFolders">Folders</option>
|
||||
</select>
|
||||
<select id="sort-by" class="shared-filter-select">
|
||||
<option value="date" data-i18n="shared_sortDate">Sort by date</option>
|
||||
<option value="name" data-i18n="shared_sortName">Sort by name</option>
|
||||
<option value="expiration" data-i18n="shared_sortExpiration">Sort by expiration</option>
|
||||
<option value="date" data-i18n="shared_sortByDate">Sort by date</option>
|
||||
<option value="name" data-i18n="shared_sortByName">Sort by name</option>
|
||||
<option value="expiration" data-i18n="shared_sortByExpiration">Sort by expiration</option>
|
||||
</select>
|
||||
<div class="shared-search-box">
|
||||
<input type="text" id="shared-search-filter" data-i18n-placeholder="shared_searchPlaceholder" placeholder="Search...">
|
||||
<input type="text" id="shared-search-filter" data-i18n-placeholder="shared_search" placeholder="Search...">
|
||||
<button id="shared-search-filter-btn" class="search-btn">🔍</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,8 +88,8 @@ const sharedView = {
|
||||
|
||||
<div id="empty-shared-state" class="empty-state" style="display:none;">
|
||||
<div class="empty-state-icon">📤</div>
|
||||
<h3 data-i18n="shared_emptyTitle">No shared items</h3>
|
||||
<p data-i18n="shared_emptyDesc">Items you share will appear here</p>
|
||||
<h3 data-i18n="shared_emptyStateTitle">No shared items</h3>
|
||||
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
|
||||
<button id="empty-go-to-files" class="button primary" data-i18n="shared_goToFiles">Go to Files</button>
|
||||
</div>
|
||||
|
||||
@@ -98,78 +97,78 @@ const sharedView = {
|
||||
<table class="shared-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="shared_columnName">Name</th>
|
||||
<th data-i18n="shared_columnType">Type</th>
|
||||
<th data-i18n="shared_columnDate">Date</th>
|
||||
<th data-i18n="shared_columnExpiration">Expiration</th>
|
||||
<th data-i18n="shared_columnPermissions">Permissions</th>
|
||||
<th data-i18n="shared_columnPassword">Password</th>
|
||||
<th data-i18n="shared_columnActions">Actions</th>
|
||||
<th data-i18n="shared_colName">Name</th>
|
||||
<th data-i18n="shared_colType">Type</th>
|
||||
<th data-i18n="shared_colDateShared">Date</th>
|
||||
<th data-i18n="shared_colExpiration">Expiration</th>
|
||||
<th data-i18n="shared_colPermissions">Permissions</th>
|
||||
<th data-i18n="shared_colPassword">Password</th>
|
||||
<th data-i18n="shared_colActions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="shared-items-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Share Edit Dialog -->
|
||||
<div id="share-dialog" class="shared-dialog">
|
||||
<!-- Share Edit Dialog (sharedView-specific) -->
|
||||
<div id="shared-view-edit-dialog" class="shared-dialog">
|
||||
<div class="shared-dialog-content">
|
||||
<div class="shared-dialog-header">
|
||||
<span id="share-dialog-icon">📄</span>
|
||||
<span id="share-dialog-name">Item</span>
|
||||
<span id="sv-dialog-icon">📄</span>
|
||||
<span id="sv-dialog-name">Item</span>
|
||||
<button class="close-dialog-btn">×</button>
|
||||
</div>
|
||||
<div class="share-link-section">
|
||||
<label data-i18n="share.linkLabel">Share Link:</label>
|
||||
<div class="share-link-input">
|
||||
<input type="text" id="share-link-url" readonly>
|
||||
<button id="copy-link-btn" class="button" data-i18n="share.copyLink">Copy</button>
|
||||
<input type="text" id="sv-share-link-url" readonly>
|
||||
<button id="sv-copy-link-btn" class="button" data-i18n="share.copyLink">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="share-permissions-section">
|
||||
<h4 data-i18n="share.permissions">Permissions</h4>
|
||||
<label><input type="checkbox" id="permission-read" checked> <span data-i18n="share.permissionRead">Read</span></label>
|
||||
<label><input type="checkbox" id="permission-write"> <span data-i18n="share.permissionWrite">Write</span></label>
|
||||
<label><input type="checkbox" id="permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
|
||||
<label><input type="checkbox" id="sv-permission-read" checked> <span data-i18n="share.permissionRead">Read</span></label>
|
||||
<label><input type="checkbox" id="sv-permission-write"> <span data-i18n="share.permissionWrite">Write</span></label>
|
||||
<label><input type="checkbox" id="sv-permission-reshare"> <span data-i18n="share.permissionReshare">Reshare</span></label>
|
||||
</div>
|
||||
<div class="share-password-section">
|
||||
<label><input type="checkbox" id="enable-password"> <span data-i18n="share.enablePassword">Password protection</span></label>
|
||||
<label><input type="checkbox" id="sv-enable-password"> <span data-i18n="share.enablePassword">Password protection</span></label>
|
||||
<div class="password-input-group">
|
||||
<input type="text" id="share-password" disabled placeholder="Enter password">
|
||||
<button id="generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
|
||||
<input type="text" id="sv-share-password" disabled placeholder="Enter password">
|
||||
<button id="sv-generate-password" class="button small" data-i18n="share.generatePassword">Generate</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="share-expiration-section">
|
||||
<label><input type="checkbox" id="enable-expiration"> <span data-i18n="share.enableExpiration">Set expiration</span></label>
|
||||
<input type="date" id="share-expiration" disabled>
|
||||
<label><input type="checkbox" id="sv-enable-expiration"> <span data-i18n="share.enableExpiration">Set expiration</span></label>
|
||||
<input type="date" id="sv-share-expiration" disabled>
|
||||
</div>
|
||||
<div class="share-actions">
|
||||
<button id="update-share-btn" class="button primary" data-i18n="share.update">Update</button>
|
||||
<button id="remove-share-btn" class="button danger" data-i18n="share.remove">Remove Share</button>
|
||||
<button id="sv-update-share-btn" class="button primary" data-i18n="share.update">Update</button>
|
||||
<button id="sv-remove-share-btn" class="button danger" data-i18n="share.remove">Remove Share</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Dialog -->
|
||||
<div id="share-notification-dialog" class="shared-dialog">
|
||||
<!-- Notification Dialog (sharedView-specific) -->
|
||||
<div id="sv-notification-dialog" class="shared-dialog">
|
||||
<div class="shared-dialog-content">
|
||||
<div class="shared-dialog-header">
|
||||
<span id="notify-dialog-icon">📧</span>
|
||||
<span id="notify-dialog-name">Item</span>
|
||||
<span id="sv-notify-dialog-icon">📧</span>
|
||||
<span id="sv-notify-dialog-name">Item</span>
|
||||
<button class="close-dialog-btn">×</button>
|
||||
</div>
|
||||
<div class="notification-form">
|
||||
<div class="form-group">
|
||||
<label data-i18n="share.notifyEmail">Email:</label>
|
||||
<input type="email" id="notification-email" placeholder="recipient@example.com">
|
||||
<input type="email" id="sv-notification-email" placeholder="recipient@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="share.notifyMessage">Message (optional):</label>
|
||||
<textarea id="notification-message" rows="3"></textarea>
|
||||
<textarea id="sv-notification-message" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-actions">
|
||||
<button id="send-notification-btn" class="button primary" data-i18n="share.notifySend">Send Notification</button>
|
||||
<button id="sv-send-notification-btn" class="button primary" data-i18n="share.notifySend">Send Notification</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,37 +199,37 @@ const sharedView = {
|
||||
if (searchBtn) searchBtn.addEventListener('click', () => this.filterAndSortItems());
|
||||
if (emptyGoToFiles) emptyGoToFiles.addEventListener('click', () => window.switchToFilesView());
|
||||
|
||||
// Share dialog
|
||||
const shareDialog = document.getElementById('share-dialog');
|
||||
// Share dialog (sharedView-specific IDs)
|
||||
const shareDialog = document.getElementById('shared-view-edit-dialog');
|
||||
if (shareDialog) {
|
||||
const closeBtn = shareDialog.querySelector('.close-dialog-btn');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.closeShareDialog());
|
||||
const copyLinkBtn = document.getElementById('copy-link-btn');
|
||||
const copyLinkBtn = document.getElementById('sv-copy-link-btn');
|
||||
if (copyLinkBtn) copyLinkBtn.addEventListener('click', () => this.copyShareLink());
|
||||
const enablePw = document.getElementById('enable-password');
|
||||
const pwField = document.getElementById('share-password');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
if (enablePw) enablePw.addEventListener('change', () => {
|
||||
if (pwField) { pwField.disabled = !enablePw.checked; if (enablePw.checked) pwField.focus(); }
|
||||
});
|
||||
const genPwBtn = document.getElementById('generate-password');
|
||||
const genPwBtn = document.getElementById('sv-generate-password');
|
||||
if (genPwBtn) genPwBtn.addEventListener('click', () => this.generatePassword());
|
||||
const enableExp = document.getElementById('enable-expiration');
|
||||
const expField = document.getElementById('share-expiration');
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
if (enableExp) enableExp.addEventListener('change', () => {
|
||||
if (expField) { expField.disabled = !enableExp.checked; if (enableExp.checked) expField.focus(); }
|
||||
});
|
||||
const updateBtn = document.getElementById('update-share-btn');
|
||||
const updateBtn = document.getElementById('sv-update-share-btn');
|
||||
if (updateBtn) updateBtn.addEventListener('click', () => this.updateSharedItem());
|
||||
const removeBtn = document.getElementById('remove-share-btn');
|
||||
const removeBtn = document.getElementById('sv-remove-share-btn');
|
||||
if (removeBtn) removeBtn.addEventListener('click', () => this.removeSharedItem());
|
||||
}
|
||||
|
||||
// Notification dialog
|
||||
const notifDialog = document.getElementById('share-notification-dialog');
|
||||
// Notification dialog (sharedView-specific IDs)
|
||||
const notifDialog = document.getElementById('sv-notification-dialog');
|
||||
if (notifDialog) {
|
||||
const closeBtn = notifDialog.querySelector('.close-dialog-btn');
|
||||
if (closeBtn) closeBtn.addEventListener('click', () => this.closeNotificationDialog());
|
||||
const sendBtn = document.getElementById('send-notification-btn');
|
||||
const sendBtn = document.getElementById('sv-send-notification-btn');
|
||||
if (sendBtn) sendBtn.addEventListener('click', () => this.sendNotification());
|
||||
}
|
||||
},
|
||||
@@ -359,19 +358,19 @@ const sharedView = {
|
||||
// Open share dialog
|
||||
openShareDialog(item) {
|
||||
this.currentItem = item;
|
||||
const shareDialog = document.getElementById('share-dialog');
|
||||
const shareDialog = document.getElementById('shared-view-edit-dialog');
|
||||
const dn = item.item_name || item.item_id || 'Unknown';
|
||||
|
||||
const iconEl = document.getElementById('share-dialog-icon');
|
||||
const nameEl = document.getElementById('share-dialog-name');
|
||||
const urlEl = document.getElementById('share-link-url');
|
||||
const enablePw = document.getElementById('enable-password');
|
||||
const pwField = document.getElementById('share-password');
|
||||
const enableExp = document.getElementById('enable-expiration');
|
||||
const expField = document.getElementById('share-expiration');
|
||||
const permRead = document.getElementById('permission-read');
|
||||
const permWrite = document.getElementById('permission-write');
|
||||
const permReshare = document.getElementById('permission-reshare');
|
||||
const iconEl = document.getElementById('sv-dialog-icon');
|
||||
const nameEl = document.getElementById('sv-dialog-name');
|
||||
const urlEl = document.getElementById('sv-share-link-url');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
const permRead = document.getElementById('sv-permission-read');
|
||||
const permWrite = document.getElementById('sv-permission-write');
|
||||
const permReshare = document.getElementById('sv-permission-reshare');
|
||||
|
||||
if (!shareDialog) return;
|
||||
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
|
||||
@@ -398,7 +397,7 @@ const sharedView = {
|
||||
},
|
||||
|
||||
closeShareDialog() {
|
||||
const d = document.getElementById('share-dialog');
|
||||
const d = document.getElementById('shared-view-edit-dialog');
|
||||
if (d) d.classList.remove('active');
|
||||
this.currentItem = null;
|
||||
},
|
||||
@@ -406,11 +405,11 @@ const sharedView = {
|
||||
openNotificationDialog(item) {
|
||||
this.currentItem = item;
|
||||
const dn = item.item_name || item.item_id || 'Unknown';
|
||||
const d = document.getElementById('share-notification-dialog');
|
||||
const iconEl = document.getElementById('notify-dialog-icon');
|
||||
const nameEl = document.getElementById('notify-dialog-name');
|
||||
const emailEl = document.getElementById('notification-email');
|
||||
const msgEl = document.getElementById('notification-message');
|
||||
const d = document.getElementById('sv-notification-dialog');
|
||||
const iconEl = document.getElementById('sv-notify-dialog-icon');
|
||||
const nameEl = document.getElementById('sv-notify-dialog-name');
|
||||
const emailEl = document.getElementById('sv-notification-email');
|
||||
const msgEl = document.getElementById('sv-notification-message');
|
||||
|
||||
if (!d) return;
|
||||
if (iconEl) iconEl.textContent = item.item_type === 'file' ? '📄' : '📁';
|
||||
@@ -421,13 +420,13 @@ const sharedView = {
|
||||
},
|
||||
|
||||
closeNotificationDialog() {
|
||||
const d = document.getElementById('share-notification-dialog');
|
||||
const d = document.getElementById('sv-notification-dialog');
|
||||
if (d) d.classList.remove('active');
|
||||
this.currentItem = null;
|
||||
},
|
||||
|
||||
copyShareLink() {
|
||||
const el = document.getElementById('share-link-url');
|
||||
const el = document.getElementById('sv-share-link-url');
|
||||
if (!el) return;
|
||||
navigator.clipboard.writeText(el.value)
|
||||
.then(() => this.showNotification(this.translate('shared_linkCopied', 'Link copied!')))
|
||||
@@ -436,8 +435,8 @@ const sharedView = {
|
||||
|
||||
// Generate secure password with crypto API
|
||||
generatePassword() {
|
||||
const pwField = document.getElementById('share-password');
|
||||
const enablePw = document.getElementById('enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
if (!pwField || !enablePw) return;
|
||||
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
@@ -456,13 +455,13 @@ const sharedView = {
|
||||
async updateSharedItem() {
|
||||
if (!this.currentItem) return;
|
||||
|
||||
const permRead = document.getElementById('permission-read');
|
||||
const permWrite = document.getElementById('permission-write');
|
||||
const permReshare = document.getElementById('permission-reshare');
|
||||
const enablePw = document.getElementById('enable-password');
|
||||
const pwField = document.getElementById('share-password');
|
||||
const enableExp = document.getElementById('enable-expiration');
|
||||
const expField = document.getElementById('share-expiration');
|
||||
const permRead = document.getElementById('sv-permission-read');
|
||||
const permWrite = document.getElementById('sv-permission-write');
|
||||
const permReshare = document.getElementById('sv-permission-reshare');
|
||||
const enablePw = document.getElementById('sv-enable-password');
|
||||
const pwField = document.getElementById('sv-share-password');
|
||||
const enableExp = document.getElementById('sv-enable-expiration');
|
||||
const expField = document.getElementById('sv-share-expiration');
|
||||
|
||||
const body = {
|
||||
permissions: {
|
||||
@@ -521,8 +520,8 @@ const sharedView = {
|
||||
// Send notification (stub)
|
||||
sendNotification() {
|
||||
if (!this.currentItem) return;
|
||||
const emailEl = document.getElementById('notification-email');
|
||||
const msgEl = document.getElementById('notification-message');
|
||||
const emailEl = document.getElementById('sv-notification-email');
|
||||
const msgEl = document.getElementById('sv-notification-message');
|
||||
const email = emailEl ? emailEl.value.trim() : '';
|
||||
const message = msgEl ? msgEl.value.trim() : '';
|
||||
|
||||
|
||||
+88
-76
@@ -23,6 +23,7 @@ const fileOps = {
|
||||
// Upload progress — notification bell integration
|
||||
// ========================================================================
|
||||
_currentBatchId: null,
|
||||
_isUploading: false, // Guard against concurrent upload calls
|
||||
|
||||
/** Start a new upload batch in the notification bell */
|
||||
_initUploadToast(totalFiles) {
|
||||
@@ -103,86 +104,97 @@ const fileOps = {
|
||||
const totalFiles = files.length;
|
||||
if (totalFiles === 0) return;
|
||||
|
||||
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
||||
const progressBar = document.querySelector('.progress-fill');
|
||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||
if (progressBar) { progressBar.style.width = '0%'; }
|
||||
|
||||
// Show upload notification
|
||||
this._initUploadToast(totalFiles);
|
||||
const batchId = this._currentBatchId;
|
||||
|
||||
let uploadedCount = 0;
|
||||
let successCount = 0;
|
||||
|
||||
for (let i = 0; i < totalFiles; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
|
||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
||||
formData.append('file', file);
|
||||
|
||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
||||
file: file.name, size: file.size
|
||||
});
|
||||
|
||||
const result = await this._uploadFileXHR(formData, batchId, file.name);
|
||||
|
||||
uploadedCount++;
|
||||
|
||||
// Legacy dropzone bar
|
||||
if (progressBar) {
|
||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||
}
|
||||
// Notify bell of per-file completion
|
||||
if (window.notifications && batchId) {
|
||||
window.notifications.fileCompleted(batchId, result.ok);
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
successCount++;
|
||||
console.log(`Successfully uploaded ${file.name}`, result.data);
|
||||
} else {
|
||||
console.error(`Upload error for ${file.name}`);
|
||||
if (result.isQuotaError) {
|
||||
const msg = result.errorMsg || window.i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
icon: 'fa-exclamation-triangle',
|
||||
iconClass: 'error',
|
||||
title: file.name,
|
||||
text: msg
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All done
|
||||
this._finishUploadToast(successCount, totalFiles);
|
||||
|
||||
// Wait for backend to persist, then reload
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// Refresh storage usage display
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try { await window.refreshUserData(); } catch (_) {}
|
||||
// Guard: prevent concurrent upload calls (e.g. double drop events)
|
||||
if (this._isUploading) {
|
||||
console.warn('Upload already in progress, ignoring duplicate call');
|
||||
return;
|
||||
}
|
||||
this._isUploading = true;
|
||||
|
||||
try {
|
||||
await window.loadFiles({ forceRefresh: true });
|
||||
} catch (reloadError) {
|
||||
console.error('Error reloading files:', reloadError);
|
||||
}
|
||||
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
||||
const progressBar = document.querySelector('.progress-fill');
|
||||
const uploadProgressDiv = document.querySelector('.upload-progress');
|
||||
if (uploadProgressDiv) { uploadProgressDiv.style.display = 'block'; }
|
||||
if (progressBar) { progressBar.style.width = '0%'; }
|
||||
|
||||
setTimeout(() => {
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
if (dropzone) dropzone.style.display = 'none';
|
||||
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
||||
}, 500);
|
||||
// Show upload notification
|
||||
this._initUploadToast(totalFiles);
|
||||
const batchId = this._currentBatchId;
|
||||
|
||||
let uploadedCount = 0;
|
||||
let successCount = 0;
|
||||
|
||||
for (let i = 0; i < totalFiles; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
|
||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
||||
formData.append('file', file);
|
||||
|
||||
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
||||
file: file.name, size: file.size
|
||||
});
|
||||
|
||||
const result = await this._uploadFileXHR(formData, batchId, file.name);
|
||||
|
||||
uploadedCount++;
|
||||
|
||||
// Legacy dropzone bar
|
||||
if (progressBar) {
|
||||
progressBar.style.width = ((uploadedCount / totalFiles) * 100) + '%';
|
||||
}
|
||||
// Notify bell of per-file completion
|
||||
if (window.notifications && batchId) {
|
||||
window.notifications.fileCompleted(batchId, result.ok);
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
successCount++;
|
||||
console.log(`Successfully uploaded ${file.name}`, result.data);
|
||||
} else {
|
||||
console.error(`Upload error for ${file.name}`);
|
||||
if (result.isQuotaError) {
|
||||
const msg = result.errorMsg || window.i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
icon: 'fa-exclamation-triangle',
|
||||
iconClass: 'error',
|
||||
title: file.name,
|
||||
text: msg
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All done
|
||||
this._finishUploadToast(successCount, totalFiles);
|
||||
|
||||
// Wait for backend to persist, then reload
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// Refresh storage usage display
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try { await window.refreshUserData(); } catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await window.loadFiles({ forceRefresh: true });
|
||||
} catch (reloadError) {
|
||||
console.error('Error reloading files:', reloadError);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
if (dropzone) dropzone.style.display = 'none';
|
||||
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
||||
}, 500);
|
||||
} finally {
|
||||
this._isUploading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -287,10 +287,15 @@ const ui = {
|
||||
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
|
||||
e._oxiHandled = true; // Mark as handled for document-level fallback
|
||||
dropzone.classList.remove('active');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
fileOps.uploadFiles(e.dataTransfer.files);
|
||||
}
|
||||
setTimeout(() => {
|
||||
dropzone.style.display = 'none';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Document-wide drag and drop
|
||||
@@ -318,6 +323,9 @@ const ui = {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('active');
|
||||
|
||||
// Skip if already handled by the dropzone handler (defensive against bubble leaks)
|
||||
if (e._oxiHandled) return;
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
fileOps.uploadFiles(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user