fix: resolve file viewer auth issues and add text file viewing support
- Fix file viewer not sending JWT auth tokens when loading files
- inlineViewer.js: already used XHR with auth (images/PDFs worked)
- fileViewer.js: was setting img.src/iframe.src directly without auth headers,
now uses fetch with Bearer token and blob URLs
- ui.js/contextMenus.js/fileRenderer.js/recent.js/favorites.js: replaced all
window.location.href = /api/files/... (unauthenticated navigation) with
authenticated viewer or fileOps.downloadFile()
- Add text file viewing support (text/*, application/json, etc.)
- New createTextViewer() in inlineViewer.js with authenticated fetch
- New loadTextViewer() in fileViewer.js with authenticated fetch
- New isViewableFile() helper in ui.js used across all entry points
- CSS styles for .inline-viewer-text-content and .file-viewer-text-content
- Translate remaining Spanish strings to English in viewer files
Fixes: text files showing 'Token not provided', images failing to load,
and text files not being previewable at all.
This commit is contained in:
@@ -123,6 +123,25 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Text viewer styles */
|
||||
.file-viewer-text-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
font-family: 'Courier New', Consolas, Monaco, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.file-viewer-loader {
|
||||
position: absolute;
|
||||
|
||||
@@ -193,6 +193,25 @@
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* Text viewer */
|
||||
.inline-viewer-text-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
font-family: 'Courier New', Consolas, Monaco, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.inline-viewer-content {
|
||||
|
||||
@@ -88,9 +88,8 @@ const contextMenus = {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(fileDetails => {
|
||||
// Check if viewable file type
|
||||
if ((fileDetails.mime_type && fileDetails.mime_type.startsWith('image/')) ||
|
||||
(fileDetails.mime_type && fileDetails.mime_type === 'application/pdf')) {
|
||||
// Check if viewable file type (images, PDFs, text files)
|
||||
if (window.ui && window.ui.isViewableFile(fileDetails)) {
|
||||
// Open with inline viewer
|
||||
if (window.inlineViewer) {
|
||||
window.inlineViewer.openFile(fileDetails);
|
||||
|
||||
+12
-4
@@ -609,9 +609,13 @@ const favorites = {
|
||||
<div class="file-info">Modified ${formattedDate.split(' ')[0]}</div>
|
||||
`;
|
||||
|
||||
// Download on click
|
||||
// View or download on click
|
||||
fileGridElement.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
} else if (window.fileOps) {
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu
|
||||
@@ -654,9 +658,13 @@ const favorites = {
|
||||
<div class="date-cell">${formattedDate}</div>
|
||||
`;
|
||||
|
||||
// Download on click
|
||||
// View or download on click
|
||||
fileListElement.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
} else if (window.fileOps) {
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu
|
||||
|
||||
@@ -304,8 +304,10 @@ class FileRenderer {
|
||||
window.inlineViewer.openFile(item);
|
||||
} else {
|
||||
console.warn('Inline viewer not available, downloading directly');
|
||||
// Fallback to direct download if viewer is not available
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
// Fallback to authenticated download if viewer is not available
|
||||
if (window.fileOps) {
|
||||
window.fileOps.downloadFile(item.id, item.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -448,8 +450,10 @@ class FileRenderer {
|
||||
window.inlineViewer.openFile(item);
|
||||
} else {
|
||||
console.warn('Inline viewer not available, downloading directly');
|
||||
// Fallback to direct download if viewer is not available
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
// Fallback to authenticated download if viewer is not available
|
||||
if (window.fileOps) {
|
||||
window.fileOps.downloadFile(item.id, item.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+143
-35
@@ -128,6 +128,9 @@ class FileViewer {
|
||||
} else if (fileData.mime_type && fileData.mime_type === 'application/pdf') {
|
||||
console.log('FileViewer: Loading PDF viewer');
|
||||
this.loadPdfViewer(fileData.id, viewerArea);
|
||||
} else if (fileData.mime_type && this.isTextViewable(fileData.mime_type)) {
|
||||
console.log('FileViewer: Loading text viewer');
|
||||
this.loadTextViewer(fileData.id, viewerArea);
|
||||
} else {
|
||||
console.log('FileViewer: Unsupported file type', fileData.mime_type);
|
||||
// For unsupported files, show download prompt
|
||||
@@ -141,25 +144,37 @@ class FileViewer {
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
loadImageViewer(fileId, container) {
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
img.className = 'file-viewer-image';
|
||||
img.src = `/api/files/${fileId}`;
|
||||
img.alt = this.fileData.name;
|
||||
|
||||
// Create loader
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'file-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// When image loads, remove loader
|
||||
img.onload = () => {
|
||||
container.removeChild(loader);
|
||||
};
|
||||
|
||||
// Add image to container
|
||||
container.appendChild(img);
|
||||
// Fetch image with auth header and create blob URL
|
||||
this.fetchFileAsBlob(fileId).then(blob => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
this.currentBlobUrl = blobUrl;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'file-viewer-image';
|
||||
img.src = blobUrl;
|
||||
img.alt = this.fileData.name;
|
||||
|
||||
img.onload = () => {
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
this.showErrorMessage(container);
|
||||
};
|
||||
|
||||
container.appendChild(img);
|
||||
}).catch(error => {
|
||||
console.error('Error loading image:', error);
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
this.showErrorMessage(container);
|
||||
});
|
||||
|
||||
// Add zoom controls to toolbar
|
||||
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
|
||||
@@ -220,25 +235,87 @@ class FileViewer {
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
loadPdfViewer(fileId, container) {
|
||||
// Create iframe for PDF viewer
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'file-viewer-pdf';
|
||||
iframe.src = `/api/files/${fileId}`;
|
||||
iframe.title = this.fileData.name;
|
||||
|
||||
// Create loader
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'file-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// When iframe loads, remove loader
|
||||
iframe.onload = () => {
|
||||
container.removeChild(loader);
|
||||
};
|
||||
// Fetch PDF with auth header and create blob URL
|
||||
this.fetchFileAsBlob(fileId).then(blob => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
this.currentBlobUrl = blobUrl;
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'file-viewer-pdf';
|
||||
iframe.src = blobUrl;
|
||||
iframe.title = this.fileData.name;
|
||||
|
||||
iframe.onload = () => {
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
};
|
||||
|
||||
container.appendChild(iframe);
|
||||
}).catch(error => {
|
||||
console.error('Error loading PDF:', error);
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
this.showErrorMessage(container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the text viewer
|
||||
*/
|
||||
async loadTextViewer(fileId, container) {
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'file-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Add iframe to container
|
||||
container.appendChild(iframe);
|
||||
try {
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const text = await response.text();
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'file-viewer-text-content';
|
||||
pre.textContent = text;
|
||||
container.appendChild(pre);
|
||||
} catch (error) {
|
||||
console.error('Error loading text:', error);
|
||||
if (loader.parentNode) container.removeChild(loader);
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a MIME type is text-viewable
|
||||
*/
|
||||
isTextViewable(mimeType) {
|
||||
if (!mimeType) return false;
|
||||
if (mimeType.startsWith('text/')) return true;
|
||||
const textTypes = [
|
||||
'application/json', 'application/xml', 'application/javascript',
|
||||
'application/x-sh', 'application/x-yaml', 'application/toml',
|
||||
'application/x-toml', 'application/sql',
|
||||
];
|
||||
return textTypes.includes(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a file as blob with auth headers
|
||||
*/
|
||||
async fetchFileAsBlob(fileId) {
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,10 +328,10 @@ class FileViewer {
|
||||
|
||||
message.innerHTML = `
|
||||
<i class="fas fa-file-download"></i>
|
||||
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'Este tipo de archivo no se puede previsualizar.'}</p>
|
||||
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'This file type cannot be previewed.'}</p>
|
||||
<button class="btn btn-primary download-btn">
|
||||
<i class="fas fa-download"></i>
|
||||
${window.i18n ? window.i18n.t('viewer.download_file') : 'Descargar archivo'}
|
||||
${window.i18n ? window.i18n.t('viewer.download_file') : 'Download file'}
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -272,14 +349,39 @@ class FileViewer {
|
||||
downloadFile() {
|
||||
if (!this.fileData) return;
|
||||
|
||||
// Create a link and simulate click
|
||||
const link = document.createElement('a');
|
||||
link.href = `/api/files/${this.fileData.id}`;
|
||||
link.download = this.fileData.name;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// Download with auth headers
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
|
||||
fetch(`/api/files/${this.fileData.id}`, { headers })
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = this.fileData.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(err => console.error('Download error:', err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showErrorMessage(container) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'file-viewer-unsupported';
|
||||
message.innerHTML = `
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p>Error loading the file. Try downloading it directly.</p>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,6 +392,12 @@ class FileViewer {
|
||||
this.fileData = null;
|
||||
this.viewerContainer.classList.remove('active');
|
||||
|
||||
// Clean up blob URL if exists
|
||||
if (this.currentBlobUrl) {
|
||||
URL.revokeObjectURL(this.currentBlobUrl);
|
||||
this.currentBlobUrl = null;
|
||||
}
|
||||
|
||||
// Reset toolbar (remove zoom controls)
|
||||
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
|
||||
const downloadBtn = toolbar.querySelector('.file-viewer-download');
|
||||
|
||||
@@ -133,6 +133,19 @@ class InlineViewer {
|
||||
// Create PDF viewer using object tag with blob URL
|
||||
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
||||
}
|
||||
else if (file.mime_type && this.isTextViewable(file.mime_type)) {
|
||||
// Hide zoom controls for text files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create text viewer using authenticated fetch
|
||||
this.createTextViewer(file, container, loader);
|
||||
}
|
||||
else {
|
||||
// Hide zoom controls for unsupported files
|
||||
controls.style.display = 'none';
|
||||
@@ -143,8 +156,8 @@ class InlineViewer {
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-file"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Este tipo de archivo no puede ser previsualizado.</p>
|
||||
<p>Haz clic en "Descargar" para obtener el archivo.</p>
|
||||
<p>This file type cannot be previewed.</p>
|
||||
<p>Click "Download" to get the file.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
@@ -154,6 +167,63 @@ class InlineViewer {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
// Check if a MIME type is text-viewable
|
||||
isTextViewable(mimeType) {
|
||||
if (!mimeType) return false;
|
||||
if (mimeType.startsWith('text/')) return true;
|
||||
const textTypes = [
|
||||
'application/json',
|
||||
'application/xml',
|
||||
'application/javascript',
|
||||
'application/x-sh',
|
||||
'application/x-yaml',
|
||||
'application/toml',
|
||||
'application/x-toml',
|
||||
'application/sql',
|
||||
];
|
||||
return textTypes.includes(mimeType);
|
||||
}
|
||||
|
||||
// Creates a text viewer using authenticated fetch
|
||||
async createTextViewer(file, container, loader) {
|
||||
try {
|
||||
console.log('Creating text viewer for:', file.name);
|
||||
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
|
||||
const response = await fetch(`/api/files/${file.id}?inline=true`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
// Create text viewer element
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'inline-viewer-text-content';
|
||||
pre.textContent = text;
|
||||
container.appendChild(pre);
|
||||
|
||||
console.log('Text viewer created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating text viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a viewer using a Blob URL to avoid content-disposition header
|
||||
async createBlobUrlViewer(file, type, container, loader) {
|
||||
try {
|
||||
@@ -269,8 +339,8 @@ class InlineViewer {
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Error al cargar el archivo.</p>
|
||||
<p>Intenta descargarlo directamente.</p>
|
||||
<p>Error loading the file.</p>
|
||||
<p>Try downloading it directly.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
|
||||
+12
-4
@@ -207,9 +207,13 @@ const recent = {
|
||||
<div class="file-info">Accessed ${formattedDate.split(' ')[0]}</div>
|
||||
`;
|
||||
|
||||
// Download on click
|
||||
// View or download on click
|
||||
fileGridElement.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
} else if (window.fileOps) {
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
|
||||
// Dispatch custom event to update recent files
|
||||
document.dispatchEvent(new CustomEvent('file-accessed', {
|
||||
@@ -257,9 +261,13 @@ const recent = {
|
||||
<div class="date-cell">${formattedDate}</div>
|
||||
`;
|
||||
|
||||
// Download on click
|
||||
// View or download on click
|
||||
fileListElement.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
} else if (window.fileOps) {
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
|
||||
// Dispatch custom event to update recent files
|
||||
document.dispatchEvent(new CustomEvent('file-accessed', {
|
||||
|
||||
+24
-8
@@ -434,6 +434,24 @@ const ui = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a file can be previewed in the viewer
|
||||
* @param {Object} file - File object with mime_type property
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isViewableFile(file) {
|
||||
if (!file || !file.mime_type) return false;
|
||||
if (file.mime_type.startsWith('image/')) return true;
|
||||
if (file.mime_type === 'application/pdf') return true;
|
||||
if (file.mime_type.startsWith('text/')) return true;
|
||||
const textTypes = [
|
||||
'application/json', 'application/xml', 'application/javascript',
|
||||
'application/x-sh', 'application/x-yaml', 'application/toml',
|
||||
'application/x-toml', 'application/sql',
|
||||
];
|
||||
return textTypes.includes(file.mime_type);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show notification
|
||||
* @param {string} title - Notification title
|
||||
@@ -957,17 +975,16 @@ const ui = {
|
||||
}
|
||||
|
||||
// Check if it's a viewable file type
|
||||
if ((file.mime_type && file.mime_type.startsWith('image/')) ||
|
||||
(file.mime_type && file.mime_type === 'application/pdf')) {
|
||||
if (this.isViewableFile(file)) {
|
||||
if (window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
} else if (window.fileViewer) {
|
||||
window.fileViewer.open(file);
|
||||
} else {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
} else {
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1051,8 +1068,7 @@ const ui = {
|
||||
}
|
||||
|
||||
// Check if it's a viewable file type
|
||||
if ((file.mime_type && file.mime_type.startsWith('image/')) ||
|
||||
(file.mime_type && file.mime_type === 'application/pdf')) {
|
||||
if (this.isViewableFile(file)) {
|
||||
// Open in the inline viewer
|
||||
if (window.inlineViewer) {
|
||||
window.inlineViewer.openFile(file);
|
||||
@@ -1061,11 +1077,11 @@ const ui = {
|
||||
window.fileViewer.open(file);
|
||||
} else {
|
||||
// No viewer available, download directly
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
} else {
|
||||
// For other file types, download directly
|
||||
window.location.href = `/api/files/${file.id}`;
|
||||
window.fileOps.downloadFile(file.id, file.name);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user