Merge pull request #139 from christianlouis/copilot/fix-files-view-auto-refresh
feat(ui): auto-refresh files table after upload and inline file preview
This commit is contained in:
+73
-1
@@ -568,7 +568,7 @@ def get_file_preview(
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=file_path,
|
path=file_path,
|
||||||
media_type=file_record.mime_type or "application/pdf",
|
media_type=file_record.mime_type or "application/pdf",
|
||||||
filename=file_record.original_filename,
|
headers={"Content-Disposition": f'inline; filename="{file_record.original_filename}"'},
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
@@ -578,6 +578,78 @@ def get_file_preview(
|
|||||||
raise HTTPException(status_code=500, detail=f"Error retrieving file preview: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error retrieving file preview: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/files/{file_id}/download")
|
||||||
|
@require_login
|
||||||
|
def download_file(
|
||||||
|
request: Request,
|
||||||
|
file_id: int,
|
||||||
|
version: str = Query("original", description="original or processed"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Download file (original or processed version) as attachment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_id: ID of the file
|
||||||
|
version: "original" for tmp file, "processed" for processed file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
File content as attachment download
|
||||||
|
"""
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the file record
|
||||||
|
file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first()
|
||||||
|
|
||||||
|
if not file_record:
|
||||||
|
raise HTTPException(status_code=404, detail=f"File with ID {file_id} not found")
|
||||||
|
|
||||||
|
if version == "original":
|
||||||
|
# Return the original file from tmp
|
||||||
|
if not file_record.local_filename or not os.path.exists(file_record.local_filename):
|
||||||
|
raise HTTPException(status_code=404, detail="Original file not found on disk")
|
||||||
|
|
||||||
|
file_path = file_record.local_filename
|
||||||
|
|
||||||
|
elif version == "processed":
|
||||||
|
# Look for processed file in /workdir/processed/
|
||||||
|
workdir = settings.workdir
|
||||||
|
processed_dir = os.path.join(workdir, "processed")
|
||||||
|
|
||||||
|
# Try to find the processed file (same hash or UUID-based naming)
|
||||||
|
base_filename = os.path.splitext(file_record.original_filename)[0]
|
||||||
|
potential_paths = [
|
||||||
|
os.path.join(processed_dir, f"{file_record.filehash}.pdf"),
|
||||||
|
os.path.join(processed_dir, f"{base_filename}_processed.pdf"),
|
||||||
|
os.path.join(processed_dir, file_record.original_filename),
|
||||||
|
]
|
||||||
|
|
||||||
|
file_path = None
|
||||||
|
for path in potential_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
file_path = path
|
||||||
|
break
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
raise HTTPException(status_code=404, detail="Processed file not found")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid version parameter. Use 'original' or 'processed'")
|
||||||
|
|
||||||
|
# Return the file with attachment disposition to trigger download
|
||||||
|
return FileResponse(
|
||||||
|
path=file_path,
|
||||||
|
media_type=file_record.mime_type or "application/pdf",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{file_record.original_filename}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error downloading file: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error downloading file: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/ui-upload")
|
@router.post("/ui-upload")
|
||||||
@require_login
|
@require_login
|
||||||
async def ui_upload(request: Request, file: UploadFile = File(...)):
|
async def ui_upload(request: Request, file: UploadFile = File(...)):
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ function updateOverallStatus(statusMessage) {
|
|||||||
|
|
||||||
if (completed === total) {
|
if (completed === total) {
|
||||||
statusMessage.textContent = `All uploads completed (${completed}/${total})`;
|
statusMessage.textContent = `All uploads completed (${completed}/${total})`;
|
||||||
|
|
||||||
|
// Trigger a custom event when all uploads are complete
|
||||||
|
const allUploadsComplete = new CustomEvent('allUploadsComplete', {
|
||||||
|
detail: { total: total, completed: completed }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(allUploadsComplete);
|
||||||
} else {
|
} else {
|
||||||
statusMessage.textContent = `Uploading files (${completed}/${total})`;
|
statusMessage.textContent = `Uploading files (${completed}/${total})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -863,12 +863,24 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 1rem;">Original File</h4>
|
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 1rem;">Original File</h4>
|
||||||
<div style="border: 2px solid #e2e8f0; border-radius: 0.5rem; overflow: hidden; background-color: #f7fafc;">
|
<div style="border: 2px solid #e2e8f0; border-radius: 0.5rem; overflow: hidden; background-color: #f7fafc;">
|
||||||
<embed src="/api/files/{{ file.id }}/preview?version=original" type="application/pdf" width="100%" height="600px" style="border: none;">
|
{% if file.mime_type and file.mime_type.startswith('image/') %}
|
||||||
|
<!-- Image preview -->
|
||||||
|
<img src="/api/files/{{ file.id }}/preview?version=original" alt="{{ file.original_filename }}" style="width: 100%; height: auto; display: block;">
|
||||||
|
{% elif file.mime_type and file.mime_type.startswith('text/') %}
|
||||||
|
<!-- Text file preview -->
|
||||||
|
<iframe src="/api/files/{{ file.id }}/preview?version=original" style="width: 100%; height: 600px; border: none;"></iframe>
|
||||||
|
{% else %}
|
||||||
|
<!-- PDF and other documents -->
|
||||||
|
<iframe src="/api/files/{{ file.id }}/preview?version=original" style="width: 100%; height: 600px; border: none;"></iframe>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 0.5rem; text-align: center;">
|
<div style="margin-top: 0.5rem; text-align: center; display: flex; gap: 1rem; justify-content: center;">
|
||||||
<a href="/api/files/{{ file.id }}/preview?version=original" target="_blank" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
<a href="/api/files/{{ file.id }}/preview?version=original" target="_blank" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
||||||
<i class="fas fa-external-link-alt"></i> Open in new tab
|
<i class="fas fa-external-link-alt"></i> Open in new tab
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/api/files/{{ file.id }}/download?version=original" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
||||||
|
<i class="fas fa-download"></i> Download
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -877,12 +889,16 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 1rem;">Processed File</h4>
|
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 1rem;">Processed File</h4>
|
||||||
<div style="border: 2px solid #e2e8f0; border-radius: 0.5rem; overflow: hidden; background-color: #f7fafc;">
|
<div style="border: 2px solid #e2e8f0; border-radius: 0.5rem; overflow: hidden; background-color: #f7fafc;">
|
||||||
<embed src="/api/files/{{ file.id }}/preview?version=processed" type="application/pdf" width="100%" height="600px" style="border: none;">
|
<!-- Processed files are typically PDFs -->
|
||||||
|
<iframe src="/api/files/{{ file.id }}/preview?version=processed" style="width: 100%; height: 600px; border: none;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 0.5rem; text-align: center;">
|
<div style="margin-top: 0.5rem; text-align: center; display: flex; gap: 1rem; justify-content: center;">
|
||||||
<a href="/api/files/{{ file.id }}/preview?version=processed" target="_blank" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
<a href="/api/files/{{ file.id }}/preview?version=processed" target="_blank" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
||||||
<i class="fas fa-external-link-alt"></i> Open in new tab
|
<i class="fas fa-external-link-alt"></i> Open in new tab
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/api/files/{{ file.id }}/download?version=processed" style="color: #3182ce; text-decoration: none; font-size: 0.875rem;">
|
||||||
|
<i class="fas fa-download"></i> Download
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -835,27 +835,17 @@
|
|||||||
|
|
||||||
// Process the dropped files
|
// Process the dropped files
|
||||||
processFiles(e.dataTransfer.files, uploadProgressContainer, uploadStatusMessage);
|
processFiles(e.dataTransfer.files, uploadProgressContainer, uploadStatusMessage);
|
||||||
|
|
||||||
// Optionally reload page after uploads complete (with a delay)
|
|
||||||
setTimeout(() => {
|
|
||||||
const fileStatuses = document.querySelectorAll('.file-status');
|
|
||||||
let allCompleted = true;
|
|
||||||
fileStatuses.forEach(status => {
|
|
||||||
if (!status.textContent.includes('Success') && !status.textContent.includes('Error')) {
|
|
||||||
allCompleted = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allCompleted && fileStatuses.length > 0) {
|
|
||||||
// Refresh the page after a short delay to show the new files
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for upload completion event and reload the page to show new files
|
||||||
|
window.addEventListener('allUploadsComplete', (e) => {
|
||||||
|
// Wait 2 seconds to let users see the success message
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
function closeUploadModal() {
|
function closeUploadModal() {
|
||||||
uploadModal.classList.remove('active');
|
uploadModal.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ class TestFilePreview:
|
|||||||
response = client.get(f"/api/files/{file_record.id}/preview?version=original")
|
response = client.get(f"/api/files/{file_record.id}/preview?version=original")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["content-type"].startswith("application/pdf")
|
assert response.headers["content-type"].startswith("application/pdf")
|
||||||
|
# Verify file is set to display inline, not download
|
||||||
|
assert "content-disposition" in response.headers
|
||||||
|
assert "inline" in response.headers["content-disposition"]
|
||||||
|
|
||||||
def test_preview_processed_file_not_found(self, client: TestClient, db_session, sample_pdf_path):
|
def test_preview_processed_file_not_found(self, client: TestClient, db_session, sample_pdf_path):
|
||||||
"""Test getting processed file preview when it doesn't exist."""
|
"""Test getting processed file preview when it doesn't exist."""
|
||||||
|
|||||||
Reference in New Issue
Block a user