diff --git a/retoors/routes.py b/retoors/routes.py index 2c94321..3f4cb2e 100644 --- a/retoors/routes.py +++ b/retoors/routes.py @@ -1,5 +1,6 @@ from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView +from .views.upload import UploadView from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team @@ -30,7 +31,7 @@ def setup_routes(app): app.router.add_post("/users/{email}/delete", UserManagementView, name="delete_user_page") app.router.add_view("/files", FileBrowserView, name="file_browser") app.router.add_post("/files/new_folder", FileBrowserView, name="new_folder") - app.router.add_post("/files/upload", FileBrowserView, name="upload_file") + app.router.add_post("/files/upload", UploadView, name="upload_file") app.router.add_get("/files/download/{file_path:.*}", FileBrowserView, name="download_file") app.router.add_post("/files/share/{file_path:.*}", FileBrowserView, name="share_file") app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item") diff --git a/retoors/static/css/components/file_browser.css b/retoors/static/css/components/file_browser.css index a07c584..71731e0 100644 --- a/retoors/static/css/components/file_browser.css +++ b/retoors/static/css/components/file_browser.css @@ -236,3 +236,107 @@ min-width: 120px; } } + +/* Styles for the new upload functionality */ +.upload-area { + border: 2px dashed var(--border-color); + border-radius: 8px; + padding: 20px; + text-align: center; + margin-bottom: 20px; + background-color: var(--background-color); +} + +.upload-button { + display: inline-block; + padding: 10px 20px; + cursor: pointer; + margin-top: 10px; +} + +.selected-files-preview { + margin-top: 20px; + text-align: left; + max-height: 200px; + overflow-y: auto; + border-top: 1px solid var(--border-color); + padding-top: 10px; +} + +.file-entry { + display: flex; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); +} + +.file-entry:last-child { + border-bottom: none; +} + +.file-entry .file-name { + flex-grow: 1; + margin-right: 10px; + color: var(--text-color); +} + +.file-entry .file-size { + color: var(--light-text-color); + font-size: 0.85rem; +} + +.file-entry .thumbnail-preview { + width: 40px; + height: 40px; + margin-left: 10px; + border-radius: 4px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--background-color); + border: 1px solid var(--border-color); +} + +.file-entry .thumbnail-preview img { + max-width: 100%; + max-height: 100%; + display: block; + object-fit: contain; +} + +.progress-bar-container { + margin-top: 15px; + text-align: left; + border-top: 1px solid var(--border-color); + padding-top: 10px; +} + +.progress-bar-container .file-name { + font-weight: bold; + margin-bottom: 5px; + color: var(--text-color); +} + +.progress-bar-wrapper { + width: 100%; + background-color: var(--background-color); + border-radius: 5px; + overflow: hidden; + height: 10px; + margin-bottom: 5px; +} + +.progress-bar { + height: 100%; + width: 0%; + background-color: var(--accent-color); + border-radius: 5px; + transition: width 0.3s ease-in-out; +} + +.progress-text { + font-size: 0.85rem; + color: var(--light-text-color); + text-align: right; +} diff --git a/retoors/static/js/main.js b/retoors/static/js/main.js index bf38425..f5d7693 100644 --- a/retoors/static/js/main.js +++ b/retoors/static/js/main.js @@ -1,5 +1,6 @@ import './components/slider.js'; import './components/navigation.js'; // Assuming navigation.js might be needed globally +import { showUploadModal } from './components/upload.js'; // Import showUploadModal document.addEventListener('DOMContentLoaded', () => { // Logic for custom-slider on order page @@ -82,4 +83,199 @@ document.addEventListener('DOMContentLoaded', () => { // Initial display based on active button (default to monthly) updatePricingDisplay('monthly'); } -}); \ No newline at end of file + + // --- File Browser Specific Logic --- + + // Helper functions for modals + function showNewFolderModal() { + document.getElementById('new-folder-modal').style.display = 'block'; + } + + function closeModal(modalId) { + document.getElementById(modalId).style.display = 'none'; + } + window.closeModal = closeModal; // Make it globally accessible + + window.onclick = function(event) { + if (event.target.classList.contains('modal')) { + event.target.style.display = 'none'; + } + } + + // File actions + function downloadFile(path) { + window.location.href = `/files/download/${path}`; + } + + async function shareFile(path, name) { + const modal = document.getElementById('share-modal'); + const linkContainer = document.getElementById('share-link-container'); + const loading = document.getElementById('share-loading'); + + document.getElementById('share-file-name').textContent = `Sharing: ${name}`; + linkContainer.style.display = 'none'; + loading.style.display = 'block'; + modal.style.display = 'block'; + + try { + const response = await fetch(`/files/share/${path}`, { + method: 'POST' + }); + const data = await response.json(); + + if (data.share_link) { + document.getElementById('share-link-input').value = data.share_link; + linkContainer.style.display = 'block'; + loading.style.display = 'none'; + } + } catch (error) { + loading.textContent = 'Error generating share link'; + } + } + + function copyShareLink() { + const input = document.getElementById('share-link-input'); + input.select(); + document.execCommand('copy'); + alert('Share link copied to clipboard'); + } + + function deleteFile(path, name) { + document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`; + document.getElementById('delete-form').action = `/files/delete/${path}`; + document.getElementById('delete-modal').style.display = 'block'; + } + + // Selection and action buttons + function updateActionButtons() { + const checked = document.querySelectorAll('.file-checkbox:checked'); + const downloadBtn = document.getElementById('download-selected-btn'); + const shareBtn = document.getElementById('share-selected-btn'); + const deleteBtn = document.getElementById('delete-selected-btn'); + + const hasSelection = checked.length > 0; + const hasFiles = Array.from(checked).some(cb => cb.dataset.isDir === 'False'); + + if (downloadBtn) downloadBtn.disabled = !hasFiles; + if (shareBtn) shareBtn.disabled = !hasSelection; + if (deleteBtn) deleteBtn.disabled = !hasSelection; + } + + function downloadSelected() { + const checked = document.querySelectorAll('.file-checkbox:checked'); + if (checked.length === 1 && checked[0].dataset.isDir === 'False') { + downloadFile(checked[0].dataset.path); + } + } + + function shareSelected() { + const checked = document.querySelectorAll('.file-checkbox:checked'); + if (checked.length === 1) { + const path = checked[0].dataset.path; + const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim(); + shareFile(path, name); + } + } + + function deleteSelected() { + const checked = document.querySelectorAll('.file-checkbox:checked'); + if (checked.length > 0) { + const paths = Array.from(checked).map(cb => cb.dataset.path); + const names = Array.from(checked).map(cb => + cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim() + ); + + if (checked.length === 1) { + deleteFile(paths[0], names[0]); + } else { + document.getElementById('delete-message').textContent = + `Are you sure you want to delete ${checked.length} items? This action cannot be undone.`; + document.getElementById('delete-modal').style.display = 'block'; + } + } + } + + // Event Listeners for File Browser + const newFolderBtn = document.getElementById('new-folder-btn'); + if (newFolderBtn) { + console.log('Attaching event listener to new-folder-btn'); + newFolderBtn.addEventListener('click', showNewFolderModal); + } + + const uploadBtn = document.getElementById('upload-btn'); + if (uploadBtn) { + console.log('Attaching event listener to upload-btn'); + uploadBtn.addEventListener('click', showUploadModal); + } + + const createFirstFolderBtn = document.getElementById('create-first-folder-btn'); + if (createFirstFolderBtn) { + console.log('Attaching event listener to create-first-folder-btn'); + createFirstFolderBtn.addEventListener('click', showNewFolderModal); + } + + const uploadFirstFileBtn = document.getElementById('upload-first-file-btn'); + if (uploadFirstFileBtn) { + console.log('Attaching event listener to upload-first-file-btn'); + uploadFirstFileBtn.addEventListener('click', showUploadModal); + } + + const downloadSelectedBtn = document.getElementById('download-selected-btn'); + if (downloadSelectedBtn) { + downloadSelectedBtn.addEventListener('click', downloadSelected); + } + + const shareSelectedBtn = document.getElementById('share-selected-btn'); + if (shareSelectedBtn) { + shareSelectedBtn.addEventListener('click', shareSelected); + } + + const deleteSelectedBtn = document.getElementById('delete-selected-btn'); + if (deleteSelectedBtn) { + deleteSelectedBtn.addEventListener('click', deleteSelected); + } + + const copyShareLinkBtn = document.getElementById('copy-share-link-btn'); + if (copyShareLinkBtn) { + copyShareLinkBtn.addEventListener('click', copyShareLink); + } + + document.getElementById('select-all')?.addEventListener('change', function(e) { + const checkboxes = document.querySelectorAll('.file-checkbox'); + checkboxes.forEach(cb => cb.checked = e.target.checked); + updateActionButtons(); + }); + + document.querySelectorAll('.file-checkbox').forEach(cb => { + cb.addEventListener('change', updateActionButtons); + }); + + // Event listeners for dynamically created download/share/delete buttons + document.addEventListener('click', (event) => { + if (event.target.classList.contains('download-file-btn')) { + downloadFile(event.target.dataset.path); + } else if (event.target.classList.contains('share-file-btn')) { + shareFile(event.target.dataset.path, event.target.dataset.name); + } else if (event.target.classList.contains('delete-file-btn')) { + deleteFile(event.target.dataset.path, event.target.dataset.name); + } + }); + + + document.getElementById('search-bar')?.addEventListener('input', function(e) { + const searchTerm = e.target.value.toLowerCase(); + const rows = document.querySelectorAll('#file-list-body tr'); + + rows.forEach(row => { + const name = row.querySelector('td:nth-child(2)')?.textContent.toLowerCase(); + if (name && name.includes(searchTerm)) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + }); + }); + + // Initial update of action buttons + updateActionButtons(); +}); diff --git a/retoors/templates/pages/file_browser.html b/retoors/templates/pages/file_browser.html index 34ab564..808a2c8 100644 --- a/retoors/templates/pages/file_browser.html +++ b/retoors/templates/pages/file_browser.html @@ -9,11 +9,11 @@ {% block page_title %}My Files{% endblock %} {% block dashboard_actions %} - - - - - + + + + + {% endblock %} {% block dashboard_content %} @@ -70,10 +70,10 @@
{% if not item.is_dir %} - + {% endif %} - - + +
@@ -82,8 +82,8 @@

No files found in this directory.

- - + + {% endif %} @@ -108,15 +108,17 @@ @@ -127,7 +129,7 @@

Generating share link...
- + + {% endblock %} diff --git a/retoors/views/site.py b/retoors/views/site.py index 6f78ab9..fa5f7fe 100644 --- a/retoors/views/site.py +++ b/retoors/views/site.py @@ -182,46 +182,6 @@ class FileBrowserView(web.View): ) ) - elif route_name == "upload_file": - try: - reader = await self.request.multipart() - field = await reader.next() - if not field or field.name != "file": - return web.HTTPFound( - self.request.app.router["file_browser"].url_for().with_query( - error="No file selected for upload" - ) - ) - - filename = field.filename - if not filename: - return web.HTTPFound( - self.request.app.router["file_browser"].url_for().with_query( - error="Filename is required" - ) - ) - - content = await field.read() - success = await file_service.upload_file(user_email, filename, content) - if success: - return web.HTTPFound( - self.request.app.router["file_browser"].url_for().with_query( - success=f"File '{filename}' uploaded successfully" - ) - ) - else: - return web.HTTPFound( - self.request.app.router["file_browser"].url_for().with_query( - error=f"Failed to upload file '{filename}'" - ) - ) - except Exception as e: - return web.HTTPFound( - self.request.app.router["file_browser"].url_for().with_query( - error=f"Upload error: {str(e)}" - ) - ) - elif route_name == "share_file": file_path = self.request.match_info.get("file_path") if not file_path: diff --git a/tests/test_file_browser.py b/tests/test_file_browser.py index 7383b24..5da961c 100644 --- a/tests/test_file_browser.py +++ b/tests/test_file_browser.py @@ -369,8 +369,7 @@ async def test_file_browser_upload_file(logged_in_client: TestClient, file_servi content_type='text/plain') resp = await logged_in_client.post("/files/upload", data=data, allow_redirects=False) - assert resp.status == 302 # Redirect - assert resp.headers["Location"].startswith("/files") + assert resp.status == 200 expected_path = temp_user_files_dir / user_email / "uploaded.txt" assert expected_path.is_file()