Files
CDN-APP-INSIDER/public/js/dashboard.js
Dinawo 2df1b28962
All checks were successful
continuous-integration/drone/push Build is passing
Update v1.2.0-beta - Dynamic context menu & permissions
 New Features:
- Dynamic permission-based context menus for files and folders
- Support for collaborative folder access control
- Upload to specific folders including shared folders
- Changelog modal for version updates
- Improved dark mode synchronization

🐛 Bug Fixes:
- Fixed context menu displaying incorrect options
- Fixed CSS !important override preventing dynamic menu behavior
- Fixed folder collaboration permission checks
- Fixed breadcrumb navigation with empty segments
- Fixed "Premature close" error loop in attachments
- Fixed missing user variable in admin routes
- Fixed avatar loading COEP policy issues

🔒 Security:
- Added security middleware (CSRF, rate limiting, input validation)
- Fixed collaboration folder access validation
- Improved shared folder permission handling

🎨 UI/UX Improvements:
- Removed Actions column from folder view
- Context menu now properly hides/shows based on permissions
- Better visual feedback for collaborative folders
- Improved upload flow with inline modals

🧹 Code Quality:
- Added collaboration data to folder routes
- Refactored context menu logic for better maintainability
- Added debug logging for troubleshooting
- Improved file upload handling with chunking support
2025-10-25 23:55:51 +02:00

1554 lines
60 KiB
JavaScript

// Dashboard JavaScript - Version corrigée
document.addEventListener('DOMContentLoaded', function() {
// Initialisation générale
initializeDashboard();
initializeContextMenu();
initializeDropdowns();
initializeGridView();
initializeCollaboration();
initializeFileHandlers();
});
// =================== INITIALISATION ===================
function initializeDashboard() {
// Formatage des tailles de fichiers
document.querySelectorAll('.file-size').forEach(function(element) {
const size = parseInt(element.getAttribute('data-size'));
element.textContent = formatFileSize(size);
});
// Événements de base
document.getElementById('searchButton')?.addEventListener('click', searchFiles);
document.getElementById('newFolderBtn')?.addEventListener('click', showNewFolderModal);
document.getElementById('themeSwitcher')?.addEventListener('click', toggleDarkMode);
// Thème
initTheme();
// Version
loadVersion();
// Double-clic sur les dossiers
document.querySelectorAll('tr[data-type="folder"] td:first-child, tr[data-type="shared-folder"] td:first-child').forEach(cell => {
cell.style.userSelect = 'none';
cell.addEventListener('dblclick', (e) => {
e.preventDefault();
const row = cell.closest('tr');
const url = row.dataset.url;
if (url) window.location.href = url;
});
});
}
// =================== MENU CONTEXTUEL ===================
function initializeContextMenu() {
const contextMenu = document.querySelector('.context-menu');
let selectedItem = null;
// Gestionnaire du clic droit
document.addEventListener('contextmenu', function(e) {
const row = e.target.closest('tr[data-type]');
if (!row) return;
e.preventDefault();
selectedItem = {
type: row.dataset.type,
name: row.dataset.name,
url: row.dataset.url,
element: row
};
adjustMenuOptions(selectedItem);
showContextMenu(e.pageX, e.pageY);
}); // Fermer le menu au clic extérieur
document.addEventListener('click', function(e) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu?.contains(e.target)) {
hideContextMenu();
}
});// Actions du menu contextuel
document.addEventListener('click', function(e) {
if (!e.target.closest('#contextMenu')) return;
e.preventDefault();
const target = e.target.closest('button, a');
if (!target || !selectedItem) return;
let action = '';
if (target.classList.contains('context-item-open')) action = 'open';
else if (target.classList.contains('context-item-rename')) action = 'rename';
else if (target.classList.contains('context-item-collaborate')) action = 'collaborate';
else if (target.classList.contains('context-item-share')) action = 'copy-link';
else if (target.classList.contains('context-item-move')) action = 'move';
else if (target.classList.contains('context-item-leave')) action = 'leave';
else if (target.classList.contains('context-item-delete')) action = 'delete';
if (action) {
handleMenuAction(action, selectedItem);
hideContextMenu();
}
}); function adjustMenuOptions(item) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu) return;
const isFile = item.type === 'file';
const isFolder = item.type === 'folder';
const isSharedFolder = item.type === 'shared-folder';
// DEBUG
console.log('🔍 Context Menu Debug:', {
itemType: item.type,
itemName: item.name,
isFile,
isFolder,
isSharedFolder
});
// Vérifier si l'utilisateur est propriétaire (pour dossiers partagés)
let isOwner = false;
if (isSharedFolder) {
const ownerCell = item.element.querySelector('td:nth-child(3) .text-muted');
isOwner = ownerCell && ownerCell.textContent.trim() === 'moi';
console.log('🔍 Shared Folder - isOwner:', isOwner);
}
// Récupérer tous les éléments du menu
const openBtn = contextMenu.querySelector('.context-item-open');
const renameBtn = contextMenu.querySelector('.context-item-rename');
const collaborateBtn = contextMenu.querySelector('.context-item-collaborate');
const shareBtn = contextMenu.querySelector('.context-item-share');
const moveBtn = contextMenu.querySelector('.context-item-move');
const leaveBtn = contextMenu.querySelector('.context-item-leave');
const separator = contextMenu.querySelector('.menu-separator');
const deleteBtn = contextMenu.querySelector('.context-item-delete');
// MASQUER TOUT PAR DÉFAUT
if (openBtn) openBtn.style.display = 'none';
if (renameBtn) renameBtn.style.display = 'none';
if (collaborateBtn) collaborateBtn.style.display = 'none';
if (shareBtn) shareBtn.style.display = 'none';
if (moveBtn) moveBtn.style.display = 'none';
if (leaveBtn) leaveBtn.style.display = 'none';
if (separator) separator.style.display = 'none';
if (deleteBtn) deleteBtn.style.display = 'none';
// AFFICHER SELON LE TYPE
if (isFile) {
// FICHIERS : Renommer, Copier le lien, Déplacer, Supprimer
console.log('✅ Affichage menu FICHIER');
if (renameBtn) renameBtn.style.display = 'flex';
if (shareBtn) shareBtn.style.display = 'flex';
if (moveBtn) moveBtn.style.display = 'flex';
if (separator) separator.style.display = 'block';
if (deleteBtn) deleteBtn.style.display = 'flex';
} else if (isFolder) {
// DOSSIERS PERSONNELS : Ouvrir, Renommer, Collaborer, Supprimer
console.log('✅ Affichage menu DOSSIER');
if (openBtn) openBtn.style.display = 'flex';
if (renameBtn) renameBtn.style.display = 'flex';
if (collaborateBtn) {
collaborateBtn.style.display = 'flex';
// Vérifier si déjà collaboratif
const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
const span = collaborateBtn.querySelector('span');
if (span) {
span.textContent = isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
}
}
if (separator) separator.style.display = 'block';
if (deleteBtn) deleteBtn.style.display = 'flex';
} else if (isSharedFolder) {
// DOSSIERS PARTAGÉS
console.log('✅ Affichage menu DOSSIER PARTAGÉ (owner:', isOwner, ')');
if (openBtn) openBtn.style.display = 'flex';
if (isOwner) {
// PROPRIÉTAIRE : Renommer, Collaborer, Supprimer
if (renameBtn) renameBtn.style.display = 'flex';
if (collaborateBtn) {
collaborateBtn.style.display = 'flex';
const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
const span = collaborateBtn.querySelector('span');
if (span) {
span.textContent = isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
}
}
if (separator) separator.style.display = 'block';
if (deleteBtn) deleteBtn.style.display = 'flex';
} else {
// INVITÉ : Quitter seulement
if (leaveBtn) leaveBtn.style.display = 'flex';
}
}
// DEBUG FINAL : afficher l'état de tous les boutons
console.log('📋 État final des boutons:', {
open: openBtn?.style.display,
rename: renameBtn?.style.display,
collaborate: collaborateBtn?.style.display,
share: shareBtn?.style.display,
move: moveBtn?.style.display,
leave: leaveBtn?.style.display,
delete: deleteBtn?.style.display
});
} function showContextMenu(x, y) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu) return;
contextMenu.style.display = 'block';
// Ajuster la position
const menuRect = contextMenu.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (x + menuRect.width > windowWidth) {
x = windowWidth - menuRect.width - 10;
}
if (y + menuRect.height > windowHeight) {
y = windowHeight - menuRect.height - 10;
}
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
}
function hideContextMenu() {
const contextMenu = document.getElementById('contextMenu');
if (contextMenu) {
contextMenu.style.display = 'none';
}
selectedItem = null;
}
function handleMenuAction(action, item) {
switch(action) {
case 'open':
if (item.type === 'folder') {
window.location.href = `/dpanel/dashboard/folder/${encodeURIComponent(item.name)}`;
} else if (item.type === 'shared-folder') {
window.location.href = item.url;
}
break;
case 'rename':
if (item.type === 'folder') {
renameFolder(item.name);
} else if (item.type === 'file') {
renameFile(item.name);
}
break;
case 'collaborate':
// Détecter si le dossier est déjà collaboratif en cherchant le badge
const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
if (isCollaborative) {
// Si déjà collaboratif, afficher les détails
showCollaborationDetails(item.name, item.type);
} else {
// Sinon, activer la collaboration
toggleCollaboration(item.name, item.type, true);
}
break;
case 'copy-link':
if (item.url) {
navigator.clipboard.writeText(item.url).then(() => {
showToast('success', 'Lien copié');
});
}
break;
case 'move':
if (item.type === 'file') {
// Utiliser la modal Bootstrap pour le déplacement
showMoveFileBootstrapModal(item.name);
}
break;
case 'leave':
if (item.type === 'shared-folder') {
leaveSharedFolder(item.name, item.element);
}
break;
case 'delete':
if (item.type === 'folder') {
confirmDeleteFolder(item.name);
} else if (item.type === 'file') {
confirmDeleteFile(item.name);
}
break;
}
}
}
// =================== DROPDOWNS ===================
function initializeDropdowns() {
// Gestionnaire pour les dropdowns Bootstrap
document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(toggle => {
toggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Fermer les autres dropdowns
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
if (menu !== this.nextElementSibling) {
menu.classList.remove('show');
}
});
// Toggle le dropdown actuel
const dropdown = this.nextElementSibling;
if (dropdown && dropdown.classList.contains('dropdown-menu')) {
// Correction du positionnement pour éviter le débordement en bas
dropdown.classList.toggle('show');
if (dropdown.classList.contains('show')) {
// Reset
dropdown.style.top = '';
dropdown.style.bottom = '';
dropdown.style.transform = '';
const rect = dropdown.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (rect.bottom > windowHeight) {
// Afficher au-dessus si déborde
dropdown.style.top = 'auto';
dropdown.style.bottom = '100%';
dropdown.style.transform = 'translateY(-8px)';
} else {
dropdown.style.top = '';
dropdown.style.bottom = '';
dropdown.style.transform = '';
}
}
}
// Gestion dynamique des options selon le type/propriétaire
const tr = this.closest('tr[data-type]');
if (tr && dropdown) {
const type = tr.getAttribute('data-type');
const owner = tr.querySelector('td:nth-child(3) .text-muted')?.textContent?.trim();
// Pour les dossiers partagés, vérifier si on est propriétaire
const isOwner = owner === 'moi';
dropdown.querySelectorAll('.dropdown-item').forEach(item => {
const action = item.textContent.trim();
// Déplacer : seulement pour les fichiers
if (action.includes('Déplacer')) {
item.style.display = (type === 'file') ? '' : 'none';
}
// Renommer/Supprimer : pas pour dossier partagé non propriétaire
if ((action.includes('Renommer') || action.includes('Supprimer')) && type === 'shared-folder' && !isOwner) {
item.style.display = 'none';
}
// Quitter : seulement pour dossier partagé non propriétaire
if (action.includes('Quitter ce dossier')) {
item.style.display = (type === 'shared-folder' && !isOwner) ? '' : 'none';
}
});
}
});
});
// Fermer les dropdowns au clic extérieur
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
menu.classList.remove('show');
});
}
});
// Empêcher la fermeture du dropdown quand on clique à l'intérieur
document.querySelectorAll('.dropdown-menu').forEach(menu => {
menu.addEventListener('click', function(e) {
e.stopPropagation();
});
});
// Action pour quitter un dossier partagé
document.querySelectorAll('.leave-shared-folder-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const folderName = this.getAttribute('data-folder-name');
const folderOwner = this.getAttribute('data-folder-owner');
Swal.fire({
title: 'Quitter ce dossier partagé ?',
text: 'Vous ne verrez plus ce dossier dans votre dashboard.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Quitter',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/dpanel/sharedfolders/leave`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderName, folderOwner })
})
.then(res => res.json())
.then(data => {
if (data.success) {
showToast('success', 'Dossier quitté');
setTimeout(() => location.reload(), 800);
} else {
showToast('error', data.error || 'Erreur');
}
})
.catch(() => showToast('error', 'Erreur réseau'));
}
});
});
});
}
// =================== VUE GRILLE ===================
function initializeGridView() {
const tableBody = document.querySelector('#fileTable tbody');
const searchContainer = document.querySelector('.flex.justify-between.items-center');
if (!tableBody || !searchContainer) return;
// Créer le bouton de basculement
const viewToggleBtn = document.createElement('button');
viewToggleBtn.className = 'btn btn-secondary ml-2 view-toggle-btn';
viewToggleBtn.innerHTML = '<i class="fas fa-th-large"></i>';
viewToggleBtn.title = 'Basculer vers la vue grille';
searchContainer.appendChild(viewToggleBtn);
let isGridView = localStorage.getItem('isGridView') === 'true'; function buildGridLayout(tr) {
if (!tr) return;
const firstCell = tr.querySelector('td:first-child');
if (!firstCell) return;
const nameDiv = firstCell.querySelector('div');
if (!nameDiv) return;
const name = nameDiv.textContent.trim();
const icon = firstCell.querySelector('i')?.cloneNode(true);
const type = tr.querySelector('td:nth-child(2)')?.textContent.trim() || '';
const size = tr.querySelector('td:nth-child(4)')?.textContent.trim() || '';
const collab = firstCell.querySelector('.collaboration-badge');
if (!icon) return;
// Créer le conteneur principal
const content = document.createElement('div');
content.className = 'icon-container';
// Créer l'icône avec arrière-plan stylé
const iconWrapper = document.createElement('div');
iconWrapper.className = 'icon';
iconWrapper.appendChild(icon);
// Créer le label
const label = document.createElement('div');
label.className = 'label';
label.textContent = name;
// Créer les détails
const details = document.createElement('div');
details.className = 'details';
details.textContent = size !== '-' ? `${type}${size}` : type;
// Assembler le conteneur
content.appendChild(iconWrapper);
content.appendChild(label);
content.appendChild(details);
// Nettoyer et remplacer le contenu
firstCell.innerHTML = '';
firstCell.appendChild(content);
// Réajouter le badge de collaboration s'il existe
if (collab) {
const clonedCollab = collab.cloneNode(true);
firstCell.appendChild(clonedCollab);
}
// Ajouter le dropdown pour la vue grille
const dropdown = tr.querySelector('.dropdown');
if (dropdown) {
const gridDropdown = dropdown.cloneNode(true);
gridDropdown.className = 'grid-dropdown dropdown';
firstCell.appendChild(gridDropdown);
}
} function toggleView() {
const currentIsGrid = tableBody.classList.contains('grid-view');
isGridView = !currentIsGrid;
if (isGridView) {
// Passer à la vue grille
tableBody.classList.add('grid-view');
// Appliquer le layout grille avec un délai pour l'animation
setTimeout(() => {
document.querySelectorAll('#fileTable tbody tr').forEach((tr, index) => {
tr.style.animationDelay = `${index * 0.05}s`;
buildGridLayout(tr);
});
}, 50);
viewToggleBtn.innerHTML = '<i class="fas fa-list"></i>';
viewToggleBtn.title = 'Basculer vers la vue liste';
} else {
// Retourner à la vue liste
tableBody.classList.remove('grid-view');
// Rafraîchir pour restaurer la vue liste originale
setTimeout(() => location.reload(), 100);
}
localStorage.setItem('isGridView', isGridView);
}
viewToggleBtn.addEventListener('click', toggleView); // Initialiser la vue si nécessaire
if (isGridView) {
tableBody.classList.add('grid-view');
// Appliquer le layout avec animation délayée
setTimeout(() => {
document.querySelectorAll('#fileTable tbody tr').forEach((tr, index) => {
tr.style.animationDelay = `${index * 0.05}s`;
buildGridLayout(tr);
});
}, 100);
viewToggleBtn.innerHTML = '<i class="fas fa-list"></i>';
viewToggleBtn.title = 'Basculer vers la vue liste';
}
}
// =================== COLLABORATION ===================
function initializeCollaboration() {
// WebSocket pour la collaboration en temps réel
try {
const ws = new WebSocket(`ws://${window.location.host}`);
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'collaborationStatus') {
updateCollaborationUI(data);
}
};
ws.onerror = function(error) {
console.warn('WebSocket error:', error);
};
} catch (error) {
console.warn('WebSocket not available:', error);
}
// Charger le statut initial
loadCollaborationStatus();
// Gestionnaires d'événements
document.querySelectorAll('.toggle-collaboration-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const itemName = this.getAttribute('data-item-name');
const itemType = this.getAttribute('data-item-type');
const isCollaborative = this.getAttribute('data-is-collaborative') === 'true';
if (isCollaborative) {
showCollaborationDetails(itemName, itemType);
} else {
toggleCollaboration(itemName, itemType, true);
}
});
});
}
async function loadCollaborationStatus() {
try {
const response = await fetch('/api/dpanel/collaboration/status');
const data = await response.json();
if (data.items) {
Object.entries(data.items).forEach(([itemId, status]) => {
const [type, name] = itemId.split('-');
updateCollaborationUI({
itemName: name,
itemType: type,
isCollaborative: status.isCollaborative,
activeUsers: status.activeUsers
});
});
}
} catch (error) {
console.error('Error loading collaboration status:', error);
}
}
function toggleCollaboration(itemName, itemType, enable) {
const itemLabel = itemType === 'folder' ? 'dossier' : 'fichier';
const performToggle = () => {
fetch('/api/dpanel/collaboration/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemName, itemType, enable })
})
.then(response => response.json())
.then(data => {
if (data.success) {
updateCollaborationUI({
itemName,
itemType,
isCollaborative: enable,
activeUsers: data.activeUsers || []
});
showToast('success', `Collaboration ${enable ? 'activée' : 'désactivée'}`);
} else {
throw new Error(data.error || 'Erreur inconnue');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors de la modification de la collaboration');
});
};
if (!enable) {
performToggle();
} else {
Swal.fire({
title: 'Activer la collaboration ?',
text: `D'autres utilisateurs pourront accéder à ce ${itemLabel} en temps réel.`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Activer',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
performToggle();
}
});
}
}
function showCollaborationDetails(itemName, itemType) {
fetch(`/api/dpanel/collaboration/details/${itemType}/${itemName}`)
.then(response => response.json())
.then(data => {
// Supprimer toute modal existante
document.querySelectorAll('.collaboration-modal').forEach(modal => modal.remove());
const modal = createCollaborationModal(itemName, itemType, data);
document.body.appendChild(modal);
// Afficher la modal avec les classes CSS modernes
modal.classList.add('show');
modal.style.display = 'flex';
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Impossible de charger les détails de la collaboration');
});
}
function createCollaborationModal(itemName, itemType, data) {
const modal = document.createElement('div');
modal.className = 'collaboration-modal';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title-wrapper">
<i class="fas fa-users modal-icon"></i>
<h5 class="modal-title">Collaboration - ${itemName}</h5>
</div>
<button type="button" class="close" onclick="this.closest('.collaboration-modal').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<div class="collaboration-section">
<div class="collaboration-section-header">
<div class="section-title-group">
<i class="fas fa-user-friends section-icon"></i>
<h6 class="collaboration-section-title">Collaborateurs</h6>
</div>
<span class="collaboration-count-badge">${data.activeUsers ? data.activeUsers.length : 0}</span>
</div>
<div class="collaboration-users-list">
${data.activeUsers && data.activeUsers.length > 0
? data.activeUsers.map(user => `
<div class="collaboration-user-item">
<div class="user-avatar-wrapper">
<img src="${user.profilePicture || getDefaultAvatar(user.name)}"
alt="${user.name}"
class="user-avatar"
onerror="this.src='${getDefaultAvatar(user.name)}'">
<div class="user-status-indicator"></div>
</div>
<div class="user-details">
<div class="user-name">${user.name}</div>
<div class="user-role">
<i class="fas fa-shield-alt"></i>
Collaborateur
</div>
</div>
<div class="user-actions">
<button class="action-btn remove-btn"
onclick="removeCollaborator('${itemName}', '${itemType}', '${user.id}')"
title="Retirer ce collaborateur">
<i class="fas fa-user-minus"></i>
</button>
</div>
</div>
`).join('')
: `<div class="collaboration-empty-state">
<i class="fas fa-user-plus empty-icon"></i>
<p>Aucun collaborateur actif</p>
<span>Ajoutez des utilisateurs pour commencer à collaborer</span>
</div>`
}
</div>
</div>
<div class="collaboration-section">
<div class="collaboration-section-header">
<div class="section-title-group">
<i class="fas fa-user-plus section-icon"></i>
<h6 class="collaboration-section-title">Ajouter un collaborateur</h6>
</div>
</div>
<div class="collaboration-search-container">
<div class="search-input-group">
<div class="search-input-wrapper">
<i class="fas fa-search search-icon"></i>
<input type="text" class="search-input search-user-input"
placeholder="Nom d'utilisateur...">
</div>
<button class="search-btn search-user-btn" type="button">
<i class="fas fa-search"></i>
<span>Rechercher</span>
</button>
</div>
<div class="search-results"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger"
onclick="toggleCollaboration('${itemName}', '${itemType}', false); this.closest('.collaboration-modal').remove();"
title="Désactiver complètement la collaboration">
<i class="fas fa-ban"></i>
<span>Désactiver collaboration</span>
</button>
<button type="button" class="btn btn-secondary"
onclick="this.closest('.collaboration-modal').remove()"
title="Fermer cette fenêtre">
<i class="fas fa-times"></i>
<span>Fermer</span>
</button>
</div>
</div>
</div>
`;
// Gestionnaire pour fermer en cliquant à l'extérieur
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
// Ajouter les gestionnaires d'événements
const searchInput = modal.querySelector('.search-user-input');
const searchBtn = modal.querySelector('.search-user-btn');
const search = () => searchCollabUser(searchInput.value.trim(), itemName, itemType, modal);
searchBtn.addEventListener('click', search);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
return modal;
}
function searchCollabUser(username, itemName, itemType, modal) {
if (!username) return;
const resultsDiv = modal.querySelector('.search-results');
resultsDiv.innerHTML = `
<div class="search-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>Recherche en cours...</span>
</div>
`;
fetch(`/api/dpanel/collaboration/searchuser?username=${encodeURIComponent(username)}`)
.then(response => response.json())
.then(result => {
if (result.found) {
resultsDiv.innerHTML = `
<div class="search-result-item">
<div class="result-user-info">
<div class="result-avatar-wrapper">
<img src="${result.user.profilePicture || getDefaultAvatar(result.user.name)}"
alt="${result.user.name}"
class="result-avatar"
onerror="this.src='${getDefaultAvatar(result.user.name)}'">
</div>
<div class="result-details">
<div class="result-name">${result.user.name}</div>
<div class="result-status">
<i class="fas fa-user-check"></i>
Utilisateur trouvé
</div>
</div>
</div>
<button class="btn btn-primary add-user-btn"
onclick="addCollaborator('${itemName}', '${itemType}', '${result.user.id}')"
title="Ajouter ce collaborateur">
<i class="fas fa-user-plus"></i>
<span>Ajouter</span>
</button>
</div>
`;
} else {
resultsDiv.innerHTML = `
<div class="search-no-result">
<i class="fas fa-user-slash"></i>
<span>Utilisateur non trouvé</span>
</div>
`;
}
})
.catch(error => {
console.error('Error:', error);
resultsDiv.innerHTML = `
<div class="search-error">
<i class="fas fa-exclamation-triangle"></i>
<span>Erreur lors de la recherche</span>
</div>
`;
});
}
function addCollaborator(itemName, itemType, userId) {
fetch('/api/dpanel/collaboration/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemName, itemType, userId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('success', 'Collaborateur ajouté');
// Rafraîchir la modal
document.querySelector('.collaboration-modal')?.remove();
showCollaborationDetails(itemName, itemType);
} else {
throw new Error(data.error || 'Erreur lors de l\'ajout');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors de l\'ajout du collaborateur');
});
}
function removeCollaborator(itemName, itemType, userId) {
fetch('/api/dpanel/collaboration/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemName, itemType, userId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('success', 'Collaborateur retiré');
// Rafraîchir la modal
document.querySelector('.collaboration-modal')?.remove();
showCollaborationDetails(itemName, itemType);
} else {
throw new Error(data.error || 'Erreur lors du retrait');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors du retrait du collaborateur');
});
}
function updateCollaborationUI(data) {
const row = document.querySelector(`tr[data-name="${data.itemName}"]`);
if (!row) return;
const button = row.querySelector('.toggle-collaboration-btn');
const nameCell = row.querySelector('td:first-child');
if (button) {
button.setAttribute('data-is-collaborative', data.isCollaborative);
button.title = data.isCollaborative ? 'Voir les collaborateurs' : 'Activer la collaboration';
}
let badge = nameCell.querySelector('.collaboration-badge');
if (data.isCollaborative) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'ml-2 badge badge-info collaboration-badge';
badge.innerHTML = '<i class="fas fa-users"></i> <span class="active-users-count"></span>';
nameCell.querySelector('div').appendChild(badge);
}
const countSpan = badge.querySelector('.active-users-count');
if (data.activeUsers && data.activeUsers.length > 0) {
badge.classList.remove('badge-secondary');
badge.classList.add('badge-info');
countSpan.textContent = ` ${data.activeUsers.length}`;
badge.title = `Collaborateurs actifs : ${data.activeUsers.map(u => u.name).join(', ')}`;
} else {
badge.classList.remove('badge-info');
badge.classList.add('badge-secondary');
countSpan.textContent = '';
badge.title = 'Aucun collaborateur actif';
}
} else if (badge) {
badge.remove();
}
}
// =================== GESTIONNAIRES DE FICHIERS ===================
function initializeFileHandlers() {
// Boutons de suppression
document.querySelectorAll('.delete-file-button, .delete-folder-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const fileName = this.dataset.fileName;
const folderName = this.dataset.folderName;
if (fileName) {
confirmDeleteFile(fileName);
} else if (folderName) {
confirmDeleteFolder(folderName);
}
});
});
// Boutons de copie
document.querySelectorAll('.copy-button').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const fileUrl = this.dataset.fileUrl;
copyFileLink(fileUrl);
});
});
// Boutons de renommage
document.querySelectorAll('.rename-file-btn, .rename-folder-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const fileName = this.dataset.fileName;
const folderName = this.dataset.folderName;
if (fileName) {
renameFile(fileName);
} else if (folderName) {
renameFolder(folderName);
}
});
});
// Boutons de déplacement
document.querySelectorAll('.move-file-btn').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const fileName = this.dataset.fileName;
// Utiliser la modal Bootstrap pour le déplacement
showMoveFileBootstrapModal(fileName);
});
}); // Gestionnaire pour le bouton confirmMoveFile dans la modal Bootstrap
const confirmMoveFileBtn = document.getElementById('confirmMoveFile');
if (confirmMoveFileBtn) {
confirmMoveFileBtn.addEventListener('click', function(e) {
e.preventDefault();
const fileNameElement = document.getElementById('moveFileName');
const folderSelectElement = document.getElementById('moveFolderSelect');
console.log('Elements found:', {
fileNameElement: !!fileNameElement,
folderSelectElement: !!folderSelectElement,
fileNameElementType: fileNameElement?.tagName,
folderSelectElementType: folderSelectElement?.tagName
});
if (!fileNameElement) {
showToast('error', 'Élément fileName non trouvé');
return;
}
if (!folderSelectElement) {
showToast('error', 'Élément folderSelect non trouvé');
return;
}
// Récupérer les valeurs brutes
const rawFileName = fileNameElement.value;
const rawFolderName = folderSelectElement.value;
console.log('Raw values:', {
rawFileName: rawFileName,
rawFolderName: rawFolderName,
rawFileNameType: typeof rawFileName,
rawFolderNameType: typeof rawFolderName,
fileNameIsString: typeof rawFileName === 'string',
folderNameIsString: typeof rawFolderName === 'string'
});
// Convertir explicitement en string et nettoyer
// Utiliser une méthode plus robuste de conversion
let fileName, folderName;
try {
fileName = (rawFileName + '').trim(); // Force conversion to string
folderName = (rawFolderName + '').trim();
} catch (e) {
console.error('Error converting values:', e);
fileName = String(rawFileName || '').trim();
folderName = String(rawFolderName || '').trim();
}
console.log('Processed values:', {
fileName: fileName,
folderName: folderName,
fileNameType: typeof fileName,
folderNameType: typeof folderName,
fileNameLength: fileName.length,
folderNameLength: folderName.length,
fileNameConstructor: fileName.constructor.name,
folderNameConstructor: folderName.constructor.name
});
if (!fileName || fileName === '') {
showToast('error', 'Aucun fichier sélectionné');
return;
}
if (!folderName || folderName === '') {
showToast('error', 'Veuillez sélectionner un dossier');
return;
}
// Fermer la modal
$('#moveFileModal').modal('hide');
// Appeler la fonction moveFile
console.log('Calling moveFile with:', { fileName, folderName });
moveFile(fileName, folderName);
});
}
}
// =================== FONCTIONS UTILITAIRES ===================
function formatFileSize(fileSizeInBytes) {
if (!fileSizeInBytes || fileSizeInBytes === 0) return '0 octets';
if (fileSizeInBytes < 1024) return fileSizeInBytes + ' octets';
else if (fileSizeInBytes < 1048576) return (fileSizeInBytes / 1024).toFixed(2) + ' Ko';
else if (fileSizeInBytes < 1073741824) return (fileSizeInBytes / 1048576).toFixed(2) + ' Mo';
else return (fileSizeInBytes / 1073741824).toFixed(2) + ' Go';
}
function getDefaultAvatar(username) {
const encodedName = encodeURIComponent(username);
return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedName}&background=%234e54c8&radius=50`;
}
function showToast(type, message) {
Swal.fire({
position: 'top-end',
icon: type,
title: message,
showConfirmButton: false,
timer: 1500,
toast: true
});
}
function searchFiles() {
const input = document.getElementById('searchInput');
const filter = input.value.toUpperCase();
const table = document.getElementById('fileTable');
const tr = table.getElementsByTagName('tr');
for (let i = 1; i < tr.length; i++) {
const td = tr[i].getElementsByTagName('td')[0];
if (td) {
const txtValue = td.textContent || td.innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
// =================== MODALES ET ACTIONS ===================
function showNewFolderModal() {
Swal.fire({
title: 'Nouveau dossier',
input: 'text',
inputPlaceholder: 'Entrer le nom du nouveau dossier',
confirmButtonText: 'Créer',
showCancelButton: true,
cancelButtonText: 'Annuler',
preConfirm: (folderName) => {
if (!folderName) {
Swal.showValidationMessage('Le nom du dossier ne peut pas être vide.');
}
return folderName;
}
}).then(result => {
if (result.isConfirmed) {
createNewFolder(result.value);
}
});
}
function createNewFolder(folderName) {
fetch('/api/dpanel/dashboard/newfolder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderName })
})
.then(async response => {
const result = await response.json();
if (response.ok && (result.success || result.message === 'Folder created successfully.')) {
showToast('success', 'Dossier créé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error(result.message || 'Erreur lors de la création');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', error.message || 'Erreur lors de la création du dossier');
});
}
function confirmDeleteFolder(folderName) {
Swal.fire({
title: 'Êtes-vous sûr?',
text: `La suppression du dossier "${folderName}" est irréversible!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Supprimer',
cancelButtonText: 'Annuler',
}).then((result) => {
if (result.isConfirmed) {
deleteFolder(folderName);
}
});
}
function deleteFolder(folderName) {
fetch(`/api/dpanel/dashboard/deletefolder/${folderName}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
showToast('success', 'Dossier supprimé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error('La suppression du dossier a échoué');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors de la suppression du dossier');
});
}
function confirmDeleteFile(filename) {
Swal.fire({
title: 'Êtes-vous sûr de vouloir supprimer ce fichier?',
text: 'Cette action est irréversible!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Supprimer'
}).then((result) => {
if (result.isConfirmed) {
deleteFile(filename);
}
});
}
function deleteFile(filename) {
fetch('/api/dpanel/dashboard/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: filename })
})
.then(response => {
if (response.ok) {
showToast('success', 'Fichier supprimé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error('La suppression du fichier a échoué');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors de la suppression du fichier');
});
}
function copyFileLink(fileUrl) {
navigator.clipboard.writeText(fileUrl).then(() => {
showToast('success', 'Lien copié !');
}, (err) => {
console.error('Erreur lors de la copie: ', err);
showToast('error', 'Erreur lors de la copie');
});
}
function renameFile(currentName) {
Swal.fire({
title: 'Renommer le fichier',
input: 'text',
inputValue: currentName,
inputPlaceholder: 'Nouveau nom',
showCancelButton: true,
confirmButtonText: 'Renommer',
cancelButtonText: 'Annuler',
inputValidator: (value) => {
if (!value) {
return 'Vous devez entrer un nom de fichier';
}
}
}).then((result) => {
if (result.isConfirmed) {
const newName = result.value;
fetch(`/api/dpanel/dashboard/rename/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentName: currentName, newName: newName })
})
.then(response => response.json())
.then(data => {
if (data.success || response.ok) {
showToast('success', 'Fichier renommé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error(data.message || 'Erreur lors du renommage');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors du renommage du fichier');
});
}
});
}
function renameFolder(currentName) {
Swal.fire({
title: 'Renommer le dossier',
input: 'text',
inputValue: currentName,
showCancelButton: true,
confirmButtonText: 'Renommer',
cancelButtonText: 'Annuler',
inputValidator: (value) => {
if (!value) {
return 'Veuillez entrer un nom de dossier';
}
}
}).then((result) => {
if (result.isConfirmed) {
const newName = result.value;
fetch('/api/dpanel/folders/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldName: currentName, newName: newName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('success', 'Dossier renommé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error(data.message || 'Erreur lors du renommage');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors du renommage du dossier');
});
}
});
}
function showMoveFileModal(fileName) {
const folders = Array.from(document.querySelectorAll('tr[data-type="folder"]'))
.map(folderRow => ({
name: folderRow.dataset.name,
value: folderRow.dataset.name
}));
Swal.fire({
title: 'Déplacer le fichier',
html: `
<select id="moveFolderSelect" class="form-control">
<option value="">Choisir un dossier...</option>
${folders.map(folder => `
<option value="${folder.value}">${folder.name}</option>
`).join('')}
</select>
`,
showCancelButton: true,
confirmButtonText: 'Déplacer',
cancelButtonText: 'Annuler',
preConfirm: () => {
const select = document.getElementById('moveFolderSelect');
const folderName = select.value;
if (!folderName) {
Swal.showValidationMessage('Veuillez sélectionner un dossier');
return false;
}
return folderName;
}
}).then((result) => {
if (result.isConfirmed && result.value) {
moveFile(fileName, result.value);
}
});
}
// Fonction alternative utilisant la modal Bootstrap
function showMoveFileBootstrapModal(fileName) {
// S'assurer que fileName est une chaîne de caractères
const fileNameStr = String(fileName).trim();
console.log('showMoveFileBootstrapModal called with:', {
original: fileName,
converted: fileNameStr,
type: typeof fileName
});
if (!fileNameStr) {
showToast('error', 'Nom de fichier invalide');
return;
}
// Définir le nom du fichier dans le champ caché
const moveFileNameElement = document.getElementById('moveFileName');
if (moveFileNameElement) {
moveFileNameElement.value = fileNameStr;
console.log('Set moveFileName value to:', moveFileNameElement.value);
} else {
console.error('Element moveFileName not found');
showToast('error', 'Erreur: Élément de formulaire manquant');
return;
}
// Afficher la modal Bootstrap
$('#moveFileModal').modal('show');
}
function moveFile(fileName, folderName) {
// S'assurer que les paramètres sont des chaînes de caractères
const fileNameStr = String(fileName).trim();
const folderNameStr = String(folderName).trim();
console.log('moveFile called with:', {
original: { fileName, folderName },
converted: { fileNameStr, folderNameStr },
types: {
originalFileName: typeof fileName,
originalFolderName: typeof folderName,
convertedFileName: typeof fileNameStr,
convertedFolderName: typeof folderNameStr
}
});
if (!fileNameStr) {
showToast('error', 'Nom de fichier invalide');
return;
}
if (!folderNameStr) {
showToast('error', 'Nom de dossier invalide');
return;
}
// Créer explicitement l'objet à envoyer
const requestBody = {};
requestBody.fileName = fileNameStr;
requestBody.folderName = folderNameStr;
// Vérification finale
console.log('Request body before sending:', {
requestBody: requestBody,
stringify: JSON.stringify(requestBody),
fileNameInBody: requestBody.fileName,
folderNameInBody: requestBody.folderName,
fileNameType: typeof requestBody.fileName,
folderNameType: typeof requestBody.folderName,
fileNameConstructor: requestBody.fileName.constructor.name,
folderNameConstructor: requestBody.folderName.constructor.name
});
const jsonPayload = JSON.stringify(requestBody);
console.log('JSON payload:', jsonPayload);
fetch('/api/dpanel/dashboard/movefile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload
})
.then(response => response.json())
.then(data => {
if (data.message === "File moved successfully" || data.success) {
showToast('success', 'Fichier déplacé avec succès');
setTimeout(() => location.reload(), 1500);
} else {
throw new Error(data.error || 'Une erreur est survenue');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur lors du déplacement du fichier');
});
}
// =================== THÈME ===================
function initTheme() {
const body = document.body;
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
setTheme('dark');
}
}
function setTheme(theme) {
const body = document.body;
if (theme === 'dark') {
body.classList.add('dark');
} else {
body.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}
function toggleDarkMode() {
const body = document.body;
if (body.classList.contains('dark')) {
setTheme('light');
} else {
setTheme('dark');
}
}
// =================== VERSION ===================
function loadVersion() {
fetch('/build-metadata')
.then(response => response.json())
.then(data => {
const versionElement = document.getElementById('version-number');
if (versionElement) {
versionElement.textContent = data.build_version;
}
})
.catch(error => {
console.error('Error fetching version:', error);
const versionElement = document.getElementById('version-number');
if (versionElement) {
versionElement.textContent = 'Version indisponible';
}
});
}
// =================== METADATA ===================
function displayMetadata() {
fetch('/build-metadata')
.then(response => response.json())
.then(metadata => {
document.getElementById('buildVersion').textContent = metadata.build_version;
document.getElementById('nodeVersion').textContent = metadata.node_version;
document.getElementById('expressVersion').textContent = metadata.express_version;
document.getElementById('buildSha').textContent = metadata.build_sha;
document.getElementById('osType').textContent = metadata.os_type;
document.getElementById('osRelease').textContent = metadata.os_release;
// Utiliser Bootstrap modal si disponible, sinon créer une modal simple
const modalElement = document.getElementById('metadataModal');
if (modalElement && typeof $ !== 'undefined' && $.fn.modal) {
$('#metadataModal').modal('show');
}
})
.catch(error => {
console.error('Failed to fetch metadata:', error);
showToast('error', 'Impossible de récupérer les métadonnées');
});
}
// Expose les fonctions globalement si nécessaire
window.displayMetadata = displayMetadata;
window.addCollaborator = addCollaborator;
window.removeCollaborator = removeCollaborator;
window.toggleCollaboration = toggleCollaboration;
// =================== FONCTIONS COLLABORATIVES ===================
function leaveSharedFolder(folderName, element) {
Swal.fire({
title: 'Quitter le dossier partagé',
text: `Êtes-vous sûr de vouloir quitter le dossier "${folderName}" ? Vous perdrez l'accès à ce dossier et ses fichiers.`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#f59e0b',
cancelButtonColor: '#6b7280',
confirmButtonText: 'Oui, quitter',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
// Extraire le propriétaire depuis l'URL du dossier partagé
const url = element.dataset.url;
const urlParts = url.split('/');
const owner = urlParts[urlParts.indexOf('shared') + 1];
fetch('/api/dpanel/sharedfolders/leave', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
folderName: folderName,
folderOwner: owner
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('success', 'Vous avez quitté le dossier partagé');
// Supprimer la ligne du tableau
element.remove();
} else {
showToast('error', data.error || 'Erreur lors de la sortie du dossier');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur de connexion');
});
}
});
}