diff --git a/static/css/components/dialogs.css b/static/css/components/dialogs.css index 2b89e032..fabec313 100644 --- a/static/css/components/dialogs.css +++ b/static/css/components/dialogs.css @@ -341,6 +341,45 @@ margin-bottom: 15px; } +/* Move dialog breadcrumb navigation */ +.move-dialog-breadcrumb { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + padding: 8px 12px; + background: #f8fafc; + border-radius: 8px; + margin-bottom: 12px; + font-size: 13px; + overflow-x: auto; +} + +.move-breadcrumb-item { + color: #4a5568; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + transition: all 0.15s ease; + white-space: nowrap; +} + +.move-breadcrumb-item:hover { + background: #e2e8f0; + color: #1a202c; +} + +.move-breadcrumb-item.current { + color: #ff5e3a; + font-weight: 600; + cursor: default; +} + +.move-breadcrumb-separator { + color: #a0aec0; + margin: 0 2px; +} + /* Folder select items in move dialog */ .folder-select-item { display: flex; @@ -373,6 +412,70 @@ color: #ff5e3a; } +/* "Select this folder" option */ +.folder-select-item.folder-select-current { + background-color: rgba(72, 187, 120, 0.1); + color: #2f855a; + font-weight: 500; +} + +.folder-select-item.folder-select-current:hover { + background-color: rgba(72, 187, 120, 0.15); +} + +.folder-select-item.folder-select-current i { + color: #48bb78; +} + +/* Navigate up option */ +.folder-select-item.folder-navigate-up { + color: #718096; + font-style: italic; +} + +.folder-select-item.folder-navigate-up:hover { + color: #4a5568; +} + +.folder-select-item.folder-navigate-up i { + color: #a0aec0; +} + +/* Folder navigation item (click to enter) */ +.folder-select-item.folder-navigate { + justify-content: space-between; +} + +.folder-select-item.folder-navigate .folder-name { + flex: 1; +} + +.folder-select-item.folder-navigate .folder-navigate-icon { + color: #a0aec0; + font-size: 12px; + opacity: 0; + transition: opacity 0.15s ease; +} + +.folder-select-item.folder-navigate:hover .folder-navigate-icon { + opacity: 1; +} + +/* Empty folder message */ +.folder-select-empty { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; + color: #a0aec0; + font-size: 14px; +} + +.folder-select-empty i { + font-size: 20px; +} + /* Custom confirm dialog */ .confirm-dialog { position: fixed; @@ -747,3 +850,80 @@ [data-theme="dark"] .share-item-info { color: #e2e8f0; } + +/* Dark mode for move dialog breadcrumb */ +[data-theme="dark"] .move-dialog-breadcrumb { + background: #0f172a; +} + +[data-theme="dark"] .move-breadcrumb-item { + color: #94a3b8; +} + +[data-theme="dark"] .move-breadcrumb-item:hover { + background: #334155; + color: #e2e8f0; +} + +[data-theme="dark"] .move-breadcrumb-item.current { + color: #ff5e3a; +} + +[data-theme="dark"] .move-breadcrumb-separator { + color: #475569; +} + +/* Dark mode for folder select items */ +[data-theme="dark"] .folder-select-item { + color: #cbd5e1; +} + +[data-theme="dark"] .folder-select-item:hover { + background-color: #334155; +} + +[data-theme="dark"] .folder-select-item.selected { + background-color: rgba(255,94,58,0.15); + color: #ff5e3a; +} + +[data-theme="dark"] .folder-select-item i { + color: #fbbf24; +} + +[data-theme="dark"] .folder-select-item.selected i { + color: #ff5e3a; +} + +[data-theme="dark"] .folder-select-item.folder-select-current { + background-color: rgba(74, 222, 128, 0.1); + color: #86efac; +} + +[data-theme="dark"] .folder-select-item.folder-select-current:hover { + background-color: rgba(74, 222, 128, 0.15); +} + +[data-theme="dark"] .folder-select-item.folder-select-current i { + color: #4ade80; +} + +[data-theme="dark"] .folder-select-item.folder-navigate-up { + color: #64748b; +} + +[data-theme="dark"] .folder-select-item.folder-navigate-up:hover { + color: #94a3b8; +} + +[data-theme="dark"] .folder-select-item.folder-navigate-up i { + color: #475569; +} + +[data-theme="dark"] .folder-select-item.folder-navigate .folder-navigate-icon { + color: #475569; +} + +[data-theme="dark"] .folder-select-empty { + color: #475569; +} diff --git a/static/js/app/ui.js b/static/js/app/ui.js index 002fafb4..9f7371b3 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -102,7 +102,7 @@ const ui = { document.body.appendChild(renameDialog); } - // Move dialog — modern + // Move dialog — modern with navigation if (!document.getElementById('move-file-dialog')) { const moveDialog = document.createElement('div'); moveDialog.className = 'rename-dialog'; @@ -115,10 +115,8 @@ const ui = {

Select destination folder:

+
-
- Root -
diff --git a/static/js/features/files/contextMenus.js b/static/js/features/files/contextMenus.js index b37dc8cb..316dfaf5 100644 --- a/static/js/features/files/contextMenus.js +++ b/static/js/features/files/contextMenus.js @@ -395,6 +395,24 @@ const contextMenus = { // Reset selection window.app.selectedTargetFolderId = ""; + // Initialize dialog navigation state + // Start at the parent of the item being moved (so user sees siblings and can navigate) + let startFolderId = null; + if (mode === 'file' && item.folder_id) { + startFolderId = item.folder_id; + } else if (mode === 'folder' && item.parent_id) { + startFolderId = item.parent_id; + } else { + // If item is at root level, start at user's home folder + startFolderId = window.app.userHomeFolderId || null; + } + + // Store the item being moved and navigation state + window.app.moveDialogItemId = item.id; + window.app.moveDialogItemMode = mode; + window.app.moveDialogCurrentFolderId = startFolderId; + window.app.moveDialogBreadcrumb = []; + // Update dialog title (preserve icon) const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header'); const titleText = mode === 'file' ? @@ -402,8 +420,8 @@ const contextMenus = { (window.i18n ? window.i18n.t('dialogs.move_folder') : 'Move folder'); dialogHeader.innerHTML = ` ${titleText}`; - // Load all available folders - await this.loadAllFolders(item.id, mode); + // Load folders for the starting location + await this.loadMoveDialogFolders(startFolderId); // Show dialog document.getElementById('move-file-dialog').style.display = 'flex'; @@ -456,11 +474,220 @@ const contextMenus = { }, /** - * Load all folders for the move dialog + * Load folders for the move dialog with navigation support + * Shows subfolders of the specified parent folder and allows navigation + * @param {string} parentFolderId - Parent folder ID to load children from (null for root) + */ + async loadMoveDialogFolders(parentFolderId) { + try { + const token = localStorage.getItem('oxicloud_token'); + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + + // Build URL - use /api/folders/{id}/contents for subfolders, or /api/folders for root + let url; + if (parentFolderId) { + url = `/api/folders/${parentFolderId}/contents`; + } else { + url = '/api/folders'; + } + + const response = await fetch(url, { headers }); + if (!response.ok) { + console.error('Failed to load folders:', response.status); + return; + } + + const data = await response.json(); + // The contents endpoint returns { folders: [...], files: [...] }, but we only need folders + const folders = Array.isArray(data) ? data : (data.folders || []); + + const folderSelectContainer = document.getElementById('folder-select-container'); + const breadcrumbContainer = document.getElementById('move-dialog-breadcrumb'); + + // Clear container + folderSelectContainer.innerHTML = ''; + + // Get current navigation state + const itemId = window.app.moveDialogItemId; + const mode = window.app.moveDialogItemMode; + const breadcrumb = window.app.moveDialogBreadcrumb || []; + + // Render breadcrumb navigation + this._renderMoveDialogBreadcrumb(breadcrumbContainer, breadcrumb, parentFolderId); + + // Option to select current folder as destination (if not at root and not the item being moved) + if (parentFolderId && parentFolderId !== itemId) { + const currentFolderOption = document.createElement('div'); + currentFolderOption.className = 'folder-select-item folder-select-current'; + currentFolderOption.innerHTML = ` + + ${window.i18n ? window.i18n.t('dialogs.select_this_folder') : 'Select this folder'} + `; + currentFolderOption.addEventListener('click', () => { + document.querySelectorAll('.folder-select-item').forEach(item => { + item.classList.remove('selected'); + }); + currentFolderOption.classList.add('selected'); + window.app.selectedTargetFolderId = parentFolderId; + }); + folderSelectContainer.appendChild(currentFolderOption); + } + + // Add "Go to parent" option if not at root + if (parentFolderId) { + const parentOption = document.createElement('div'); + parentOption.className = 'folder-select-item folder-navigate-up'; + parentOption.innerHTML = ` + + ${window.i18n ? window.i18n.t('dialogs.go_to_parent') : '.. (parent folder)'} + `; + parentOption.addEventListener('click', () => { + // Navigate to parent folder + const currentBreadcrumb = window.app.moveDialogBreadcrumb || []; + if (currentBreadcrumb.length > 0) { + // Remove current folder from breadcrumb + currentBreadcrumb.pop(); + const parentFolder = currentBreadcrumb.length > 0 + ? currentBreadcrumb[currentBreadcrumb.length - 1] + : null; + window.app.moveDialogBreadcrumb = currentBreadcrumb; + window.app.moveDialogCurrentFolderId = parentFolder ? parentFolder.id : null; + this.loadMoveDialogFolders(parentFolder ? parentFolder.id : null); + } else { + // Go to root (home folder) + window.app.moveDialogCurrentFolderId = window.app.userHomeFolderId || null; + this.loadMoveDialogFolders(window.app.userHomeFolderId || null); + } + }); + folderSelectContainer.appendChild(parentOption); + } + + // Add subfolders (clicking navigates INTO the folder) + folders.forEach(folder => { + // Skip the item being moved (to prevent moving a folder into itself) + if (mode === 'folder' && folder.id === itemId) { + return; + } + + const folderItem = document.createElement('div'); + folderItem.className = 'folder-select-item folder-navigate'; + folderItem.dataset.folderId = folder.id; + folderItem.innerHTML = ` + + ${escapeHtml(folder.name)} + + `; + + // Click navigates INTO this folder + folderItem.addEventListener('click', () => { + // Add to breadcrumb + const breadcrumb = window.app.moveDialogBreadcrumb || []; + breadcrumb.push({ id: folder.id, name: folder.name }); + window.app.moveDialogBreadcrumb = breadcrumb; + window.app.moveDialogCurrentFolderId = folder.id; + this.loadMoveDialogFolders(folder.id); + }); + + folderSelectContainer.appendChild(folderItem); + }); + + // Show "no subfolders" message if empty + if (folders.length === 0 && !parentFolderId) { + const emptyMsg = document.createElement('div'); + emptyMsg.className = 'folder-select-empty'; + emptyMsg.innerHTML = ` ${window.i18n ? window.i18n.t('dialogs.no_subfolders') : 'No subfolders'}`; + folderSelectContainer.appendChild(emptyMsg); + } + + // Set default selection to current folder + window.app.selectedTargetFolderId = parentFolderId || ''; + + // Translate new elements + if (window.i18n && window.i18n.translateElement) { + window.i18n.translateElement(folderSelectContainer); + } + } catch (error) { + console.error('Error loading folders:', error); + } + }, + + /** + * Render breadcrumb navigation for move dialog + */ + _renderMoveDialogBreadcrumb(container, breadcrumb, currentFolderId) { + if (!container) return; + container.innerHTML = ''; + + const homeFolderId = window.app.userHomeFolderId; + const homeFolderName = window.app.userHomeFolderName || 'Home'; + + // Home icon (click to go to home folder) + const homeItem = document.createElement('span'); + homeItem.className = 'move-breadcrumb-item'; + homeItem.innerHTML = ''; + homeItem.addEventListener('click', () => { + window.app.moveDialogBreadcrumb = []; + window.app.moveDialogCurrentFolderId = homeFolderId || null; + this.loadMoveDialogFolders(homeFolderId || null); + }); + container.appendChild(homeItem); + + // Home folder name + if (homeFolderName) { + const separator = document.createElement('span'); + separator.className = 'move-breadcrumb-separator'; + separator.textContent = '>'; + container.appendChild(separator); + + const homeNameItem = document.createElement('span'); + homeNameItem.className = 'move-breadcrumb-item'; + if (breadcrumb.length === 0) { + homeNameItem.classList.add('current'); + } + homeNameItem.textContent = homeFolderName; + if (breadcrumb.length > 0) { + homeNameItem.addEventListener('click', () => { + window.app.moveDialogBreadcrumb = []; + window.app.moveDialogCurrentFolderId = homeFolderId || null; + this.loadMoveDialogFolders(homeFolderId || null); + }); + } + container.appendChild(homeNameItem); + } + + // Breadcrumb path + breadcrumb.forEach((segment, index) => { + const separator = document.createElement('span'); + separator.className = 'move-breadcrumb-separator'; + separator.textContent = '>'; + container.appendChild(separator); + + const item = document.createElement('span'); + item.className = 'move-breadcrumb-item'; + if (index === breadcrumb.length - 1) { + item.classList.add('current'); + } + item.textContent = segment.name; + + // Click to navigate back to this level + if (index < breadcrumb.length - 1) { + item.addEventListener('click', () => { + window.app.moveDialogBreadcrumb = breadcrumb.slice(0, index + 1); + window.app.moveDialogCurrentFolderId = segment.id; + this.loadMoveDialogFolders(segment.id); + }); + } + container.appendChild(item); + }); + }, + + /** + * Load all folders for the move dialog (legacy - kept for batch operations) * @param {string} itemId - ID of the item being moved * @param {string} mode - 'file' or 'folder' */ async loadAllFolders(itemId, mode) { + // For batch mode, use the old behavior but start from home folder try { const token = localStorage.getItem('oxicloud_token'); const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; @@ -469,32 +696,15 @@ const contextMenus = { const folders = await response.json(); const folderSelectContainer = document.getElementById('folder-select-container'); - // Clear container except root option - folderSelectContainer.innerHTML = ` -
- Root -
- `; - - // Select root by default - window.app.selectedTargetFolderId = ""; + // Clear container + folderSelectContainer.innerHTML = ''; // Add all available folders if (Array.isArray(folders)) { + const excludeIds = itemId ? [itemId] : []; folders.forEach(folder => { // Skip folders that would create cycles - if (mode === 'folder' && folder.id === itemId) { - return; - } - - // Skip current folder of the item - if (mode === 'file' && window.app.contextMenuTargetFile && - folder.id === window.app.contextMenuTargetFile.folder_id) { - return; - } - - if (mode === 'folder' && window.app.contextMenuTargetFolder && - folder.id === window.app.contextMenuTargetFolder.parent_id) { + if (excludeIds.includes(folder.id)) { return; } @@ -518,15 +728,8 @@ const contextMenus = { }); } - // Event for root option - const rootOption = folderSelectContainer.querySelector('.folder-select-item'); - rootOption.addEventListener('click', () => { - document.querySelectorAll('.folder-select-item').forEach(item => { - item.classList.remove('selected'); - }); - rootOption.classList.add('selected'); - window.app.selectedTargetFolderId = ""; - }); + // Set default selection + window.app.selectedTargetFolderId = ""; // Translate new elements (scoped to container) if (window.i18n && window.i18n.translateElement) { diff --git a/static/locales/en.json b/static/locales/en.json index 8dd5f3be..2cbe3d97 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -202,6 +202,9 @@ "move_file": "Move file", "move_folder": "Move folder", "select_destination": "Select destination folder:", + "select_this_folder": "Select this folder", + "go_to_parent": ".. (parent folder)", + "no_subfolders": "No subfolders", "root": "Root", "delete_confirmation": "Are you sure you want to delete", "and_contents": "and all its contents", diff --git a/static/locales/es.json b/static/locales/es.json index 2b991bab..9029bd8b 100644 --- a/static/locales/es.json +++ b/static/locales/es.json @@ -202,6 +202,9 @@ "move_file": "Mover archivo", "move_folder": "Mover carpeta", "select_destination": "Selecciona la carpeta destino:", + "select_this_folder": "Seleccionar esta carpeta", + "go_to_parent": ".. (carpeta superior)", + "no_subfolders": "Sin subcarpetas", "root": "Raíz", "delete_confirmation": "¿Estás seguro de que quieres eliminar", "and_contents": "y todo su contenido",