file component update
This commit is contained in:
parent
36e1f4663b
commit
19f360e3f0
5 changed files with 591 additions and 238 deletions
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.background": "#221489",
|
||||
"titleBar.activeBackground": "#301DC0",
|
||||
"titleBar.activeForeground": "#F9F9FE"
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface AuditNote {
|
||||
note: string;
|
||||
|
@ -121,6 +122,20 @@ export default function ReimbursementList() {
|
|||
console.log('Number of requests:', requests.length);
|
||||
}, [requests]);
|
||||
|
||||
// Add a useEffect to log preview URL and filename changes
|
||||
useEffect(() => {
|
||||
console.log('Preview URL changed:', previewUrl);
|
||||
console.log('Preview filename changed:', previewFilename);
|
||||
}, [previewUrl, previewFilename]);
|
||||
|
||||
// Add a useEffect to log when the preview modal is shown/hidden
|
||||
useEffect(() => {
|
||||
console.log('Show preview changed:', showPreview);
|
||||
if (showPreview) {
|
||||
console.log('Selected receipt:', selectedReceipt);
|
||||
}
|
||||
}, [showPreview, selectedReceipt]);
|
||||
|
||||
const fetchReimbursements = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
@ -241,27 +256,88 @@ export default function ReimbursementList() {
|
|||
|
||||
const handlePreviewFile = async (request: ReimbursementRequest, receiptId: string) => {
|
||||
try {
|
||||
console.log('Previewing file for receipt ID:', receiptId);
|
||||
const pb = auth.getPocketBase();
|
||||
const fileManager = FileManager.getInstance();
|
||||
|
||||
// Set the selected request
|
||||
setSelectedRequest(request);
|
||||
|
||||
// Check if we already have the receipt details in our map
|
||||
if (receiptDetailsMap[receiptId]) {
|
||||
console.log('Using cached receipt details');
|
||||
// Use the cached receipt details
|
||||
setSelectedReceipt(receiptDetailsMap[receiptId]);
|
||||
|
||||
// Get the file URL using the PocketBase URL and collection info
|
||||
const url = `${pb.baseUrl}/api/files/receipts/${receiptId}/${receiptDetailsMap[receiptId].file}`;
|
||||
// Check if the receipt has a file
|
||||
if (!receiptDetailsMap[receiptId].file) {
|
||||
console.error('Receipt has no file attached');
|
||||
toast.error('This receipt has no file attached');
|
||||
setPreviewUrl('');
|
||||
setPreviewFilename('');
|
||||
setShowPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the file URL with token for protected files
|
||||
console.log('Getting file URL with token');
|
||||
const url = await fileManager.getFileUrlWithToken(
|
||||
'receipts',
|
||||
receiptId,
|
||||
receiptDetailsMap[receiptId].file,
|
||||
true // Use token for protected files
|
||||
);
|
||||
|
||||
// Check if the URL is empty
|
||||
if (!url) {
|
||||
console.error('Failed to get file URL: Empty URL returned');
|
||||
toast.error('Failed to load receipt: Could not generate file URL');
|
||||
// Still show the preview modal but with empty URL to display the error message
|
||||
setPreviewUrl('');
|
||||
setPreviewFilename(receiptDetailsMap[receiptId].file || '');
|
||||
setShowPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Got URL:', url.substring(0, 50) + '...');
|
||||
|
||||
// Set the preview URL and filename
|
||||
setPreviewUrl(url);
|
||||
setPreviewFilename(receiptDetailsMap[receiptId].file);
|
||||
|
||||
// Show the preview modal
|
||||
setShowPreview(true);
|
||||
|
||||
// Log the current state
|
||||
console.log('Current state after setting:', {
|
||||
previewUrl: url,
|
||||
previewFilename: receiptDetailsMap[receiptId].file,
|
||||
showPreview: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not in the map, get the receipt record using its ID
|
||||
console.log('Fetching receipt details from server');
|
||||
const receiptRecord = await pb.collection('receipts').getOne(receiptId, {
|
||||
$autoCancel: false
|
||||
});
|
||||
|
||||
if (receiptRecord) {
|
||||
console.log('Receipt record found:', receiptRecord.id);
|
||||
console.log('Receipt file:', receiptRecord.file);
|
||||
|
||||
// Check if the receipt has a file
|
||||
if (!receiptRecord.file) {
|
||||
console.error('Receipt has no file attached');
|
||||
toast.error('This receipt has no file attached');
|
||||
setPreviewUrl('');
|
||||
setPreviewFilename('');
|
||||
setShowPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the itemized expenses if it's a string
|
||||
const itemizedExpenses = typeof receiptRecord.itemized_expenses === 'string'
|
||||
? JSON.parse(receiptRecord.itemized_expenses)
|
||||
|
@ -290,16 +366,51 @@ export default function ReimbursementList() {
|
|||
|
||||
setSelectedReceipt(receiptDetails);
|
||||
|
||||
// Get the file URL using the PocketBase URL and collection info
|
||||
const url = `${pb.baseUrl}/api/files/receipts/${receiptRecord.id}/${receiptRecord.file}`;
|
||||
// Get the file URL with token for protected files
|
||||
console.log('Getting file URL with token for new receipt');
|
||||
const url = await fileManager.getFileUrlWithToken(
|
||||
'receipts',
|
||||
receiptRecord.id,
|
||||
receiptRecord.file,
|
||||
true // Use token for protected files
|
||||
);
|
||||
|
||||
// Check if the URL is empty
|
||||
if (!url) {
|
||||
console.error('Failed to get file URL: Empty URL returned');
|
||||
toast.error('Failed to load receipt: Could not generate file URL');
|
||||
// Still show the preview modal but with empty URL to display the error message
|
||||
setPreviewUrl('');
|
||||
setPreviewFilename(receiptRecord.file || '');
|
||||
setShowPreview(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Got URL:', url.substring(0, 50) + '...');
|
||||
|
||||
// Set the preview URL and filename
|
||||
setPreviewUrl(url);
|
||||
setPreviewFilename(receiptRecord.file);
|
||||
|
||||
// Show the preview modal
|
||||
setShowPreview(true);
|
||||
|
||||
// Log the current state
|
||||
console.log('Current state after setting:', {
|
||||
previewUrl: url,
|
||||
previewFilename: receiptRecord.file,
|
||||
showPreview: true
|
||||
});
|
||||
} else {
|
||||
throw new Error('Receipt not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading receipt:', error);
|
||||
toast.error('Failed to load receipt. Please try again.');
|
||||
// Show the preview modal with empty URL to display the error message
|
||||
setPreviewUrl('');
|
||||
setPreviewFilename('');
|
||||
setShowPreview(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -705,11 +816,25 @@ export default function ReimbursementList() {
|
|||
</motion.a>
|
||||
</div>
|
||||
<div className="bg-base-200/50 backdrop-blur-sm rounded-lg p-4 shadow-sm">
|
||||
<FilePreview
|
||||
url={previewUrl}
|
||||
filename={previewFilename}
|
||||
isModal={false}
|
||||
/>
|
||||
{previewUrl ? (
|
||||
<FilePreview
|
||||
url={previewUrl}
|
||||
filename={previewFilename}
|
||||
isModal={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center p-8 text-center space-y-4">
|
||||
<div className="bg-warning/20 p-4 rounded-full">
|
||||
<Icon icon="heroicons:exclamation-triangle" className="h-12 w-12 text-warning" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Receipt Image Not Available</h3>
|
||||
<p className="text-base-content/70 max-w-md">
|
||||
The receipt image could not be loaded. This might be due to permission issues or the file may not exist.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import FilePreview from '../universal/FilePreview';
|
|||
import { Get } from '../../../scripts/pocketbase/Get';
|
||||
import { Update } from '../../../scripts/pocketbase/Update';
|
||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||
import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { Receipt as SchemaReceipt, User, Reimbursement } from '../../../schemas/pocketbase';
|
||||
|
@ -70,6 +71,7 @@ export default function ReimbursementManagementPortal() {
|
|||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [rejectingId, setRejectingId] = useState<string | null>(null);
|
||||
const [receiptUrl, setReceiptUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const auth = Authentication.getInstance();
|
||||
|
@ -379,16 +381,34 @@ export default function ReimbursementManagementPortal() {
|
|||
}
|
||||
};
|
||||
|
||||
const toggleReceipt = (receiptId: string) => {
|
||||
setExpandedReceipts(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(receiptId)) {
|
||||
next.delete(receiptId);
|
||||
} else {
|
||||
next.add(receiptId);
|
||||
const toggleReceipt = async (receiptId: string) => {
|
||||
if (expandedReceipts.has(receiptId)) {
|
||||
// If already expanded, collapse it
|
||||
const newSet = new Set(expandedReceipts);
|
||||
newSet.delete(receiptId);
|
||||
setExpandedReceipts(newSet);
|
||||
setSelectedReceipt(null);
|
||||
} else {
|
||||
// If not expanded, expand it
|
||||
const newSet = new Set(expandedReceipts);
|
||||
newSet.add(receiptId);
|
||||
setExpandedReceipts(newSet);
|
||||
|
||||
// Set the selected receipt
|
||||
const receipt = receipts[receiptId];
|
||||
if (receipt) {
|
||||
setSelectedReceipt(receipt);
|
||||
|
||||
// Get the receipt URL and update the state
|
||||
try {
|
||||
const url = await getReceiptUrl(receipt);
|
||||
setReceiptUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Error getting receipt URL:', error);
|
||||
setReceiptUrl('');
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update the auditReceipt function
|
||||
|
@ -404,6 +424,15 @@ export default function ReimbursementManagementPortal() {
|
|||
const receipt = receipts[receiptId];
|
||||
if (!receipt) throw new Error('Receipt not found');
|
||||
|
||||
// Get the receipt URL and update the state
|
||||
try {
|
||||
const url = await getReceiptUrl(receipt);
|
||||
setReceiptUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Error getting receipt URL:', error);
|
||||
setReceiptUrl('');
|
||||
}
|
||||
|
||||
const updatedAuditors = [...new Set([...receipt.audited_by, userId])];
|
||||
|
||||
await update.updateFields('receipts', receiptId, {
|
||||
|
@ -441,6 +470,8 @@ export default function ReimbursementManagementPortal() {
|
|||
}
|
||||
}));
|
||||
|
||||
setSelectedReceipt(receipt);
|
||||
setShowReceiptModal(true);
|
||||
toast.success('Receipt audited successfully');
|
||||
} catch (error) {
|
||||
console.error('Error auditing receipt:', error);
|
||||
|
@ -463,10 +494,15 @@ export default function ReimbursementManagementPortal() {
|
|||
});
|
||||
};
|
||||
|
||||
const getReceiptUrl = (receipt: ExtendedReceipt): string => {
|
||||
const getReceiptUrl = async (receipt: ExtendedReceipt): Promise<string> => {
|
||||
try {
|
||||
const pb = Authentication.getInstance().getPocketBase();
|
||||
return pb.files.getURL(receipt, receipt.file);
|
||||
const fileManager = FileManager.getInstance();
|
||||
return await fileManager.getFileUrlWithToken(
|
||||
'receipts',
|
||||
receipt.id,
|
||||
receipt.file,
|
||||
true // Use token for protected files
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting receipt URL:', error);
|
||||
return '';
|
||||
|
@ -1648,7 +1684,7 @@ export default function ReimbursementManagementPortal() {
|
|||
</div>
|
||||
<div className="px-4 py-0">
|
||||
<FilePreview
|
||||
url={getReceiptUrl(selectedReceipt)}
|
||||
url={receiptUrl}
|
||||
filename={`Receipt from ${selectedReceipt.location_name}`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
|
|||
import { Icon } from "@iconify/react";
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||
|
||||
// Cache for file content
|
||||
const contentCache = new Map<string, { content: string | 'image' | 'video' | 'pdf', fileType: string, timestamp: number }>();
|
||||
|
@ -36,6 +37,19 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
return `${truncatedName}...${extension ? `.${extension}` : ''}`;
|
||||
}, [filename]);
|
||||
|
||||
// Update URL and filename when props change
|
||||
useEffect(() => {
|
||||
console.log('FilePreview props changed:', { initialUrl, initialFilename });
|
||||
if (initialUrl !== url) {
|
||||
console.log('URL changed from props:', initialUrl);
|
||||
setUrl(initialUrl);
|
||||
}
|
||||
if (initialFilename !== filename) {
|
||||
console.log('Filename changed from props:', initialFilename);
|
||||
setFilename(initialFilename);
|
||||
}
|
||||
}, [initialUrl, initialFilename, url, filename]);
|
||||
|
||||
// Intersection Observer callback
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
|
@ -55,7 +69,20 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
}, []);
|
||||
|
||||
const loadContent = useCallback(async () => {
|
||||
if (!url || !filename) return;
|
||||
if (!url) {
|
||||
// Don't log a warning if URL is empty during initial component mount
|
||||
// This is a normal state before the URL is set
|
||||
setError('No file URL provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
console.warn('Cannot load content: Filename is empty');
|
||||
setError('No filename provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading content for:', { url, filename });
|
||||
setLoading(true);
|
||||
|
@ -72,9 +99,61 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if it's likely an image based on filename extension
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
const fileExtension = filename.split('.').pop()?.toLowerCase() || '';
|
||||
const isProbablyImage = imageExtensions.includes(fileExtension);
|
||||
|
||||
if (isProbablyImage) {
|
||||
// Try loading as an image first to bypass CORS issues
|
||||
try {
|
||||
console.log('Trying to load as image:', url);
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous'; // Try anonymous mode first
|
||||
|
||||
// Create a promise to handle image loading
|
||||
const imageLoaded = new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve('image');
|
||||
img.onerror = (e) => reject(new Error('Failed to load image'));
|
||||
});
|
||||
|
||||
img.src = url;
|
||||
|
||||
// Wait for image to load
|
||||
await imageLoaded;
|
||||
|
||||
// If we get here, image loaded successfully
|
||||
setContent('image');
|
||||
setFileType('image/' + fileExtension);
|
||||
|
||||
// Cache the content
|
||||
contentCache.set(cacheKey, {
|
||||
content: 'image',
|
||||
fileType: 'image/' + fileExtension,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
} catch (imgError) {
|
||||
console.warn('Failed to load as image, falling back to fetch:', imgError);
|
||||
// Continue to fetch method
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching file...');
|
||||
const response = await fetch(url);
|
||||
console.log('Fetching file from URL:', url);
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache', // Bypass cache
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('File fetch failed with status:', response.status, response.statusText);
|
||||
throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
console.log('Received content type:', contentType);
|
||||
setFileType(contentType);
|
||||
|
@ -112,13 +191,15 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
}, [url, filename]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible || !isModal) { // Load content immediately if not in modal
|
||||
// Only attempt to load content if URL is not empty
|
||||
if ((isVisible || !isModal) && url) {
|
||||
loadContent();
|
||||
}
|
||||
}, [isVisible, loadContent, isModal]);
|
||||
}, [isVisible, loadContent, isModal, url]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('FilePreview component mounted');
|
||||
console.log('FilePreview component mounted or updated with URL:', url);
|
||||
console.log('Filename:', filename);
|
||||
|
||||
if (isModal) {
|
||||
const handleStateChange = (event: CustomEvent<{ url: string; filename: string }>) => {
|
||||
|
@ -145,6 +226,14 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
}
|
||||
}, [isModal, initialUrl, initialFilename]);
|
||||
|
||||
// Add a new effect to handle URL changes
|
||||
useEffect(() => {
|
||||
if (url && isVisible) {
|
||||
console.log('URL changed, loading content:', url);
|
||||
loadContent();
|
||||
}
|
||||
}, [url, isVisible, loadContent]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
@ -250,44 +339,26 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
}
|
||||
|
||||
try {
|
||||
return hljs.highlight(code, { language }).value;
|
||||
// Use highlight.js to highlight the code
|
||||
const highlighted = hljs.highlight(code, { language }).value;
|
||||
return highlighted;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to highlight code for language ${language}:`, error);
|
||||
return code;
|
||||
console.warn(`Failed to highlight code as ${language}, falling back to plaintext`);
|
||||
return hljs.highlight(code, { language: 'plaintext' }).value;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatCodeWithLineNumbers = useCallback((code: string, language: string) => {
|
||||
// Special handling for CSV files
|
||||
if (language === 'csv') {
|
||||
return renderCSVTable(code);
|
||||
}
|
||||
const highlighted = highlightCode(code, language);
|
||||
const lines = highlighted.split('\n').slice(0, visibleLines);
|
||||
|
||||
const lines = code.split('\n');
|
||||
const totalLines = lines.length;
|
||||
const linesToShow = Math.min(visibleLines, totalLines);
|
||||
|
||||
let formattedCode = lines
|
||||
.slice(0, linesToShow)
|
||||
.map((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
const highlightedLine = highlightCode(line, language);
|
||||
return `<div class="table-row ">
|
||||
<div class="table-cell text-right pr-4 select-none text-base-content/50 text-sm border-r border-base-content/10">${lineNumber}</div>
|
||||
<div class="table-cell pl-4 whitespace-pre">${highlightedLine || ' '}</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (linesToShow < totalLines) {
|
||||
formattedCode += `<div class="table-row ">
|
||||
<div class="table-cell"></div>
|
||||
<div class="table-cell pl-4 pt-2 text-base-content/70">... ${totalLines - linesToShow} more lines</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return formattedCode;
|
||||
}, [highlightCode, visibleLines, renderCSVTable]);
|
||||
return lines.map((line, i) =>
|
||||
`<div class="table-row">
|
||||
<div class="table-cell text-right pr-4 select-none opacity-50 w-12">${i + 1}</div>
|
||||
<div class="table-cell">${line || ' '}</div>
|
||||
</div>`
|
||||
).join('');
|
||||
}, [visibleLines, highlightCode]);
|
||||
|
||||
const handleShowMore = useCallback(() => {
|
||||
setVisibleLines(prev => Math.min(prev + CHUNK_SIZE, content?.split('\n').length || 0));
|
||||
|
@ -297,84 +368,104 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
setVisibleLines(INITIAL_LINES_TO_SHOW);
|
||||
}, []);
|
||||
|
||||
// If URL is empty, show a message
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="file-preview-container bg-base-100 rounded-lg shadow-md overflow-hidden">
|
||||
<div className="p-6 flex flex-col items-center justify-center text-center">
|
||||
<Icon icon="heroicons:exclamation-triangle" className="h-12 w-12 text-warning mb-3" />
|
||||
<h3 className="text-lg font-semibold mb-2">No File URL Provided</h3>
|
||||
<p className="text-base-content/70">Please check if the file exists or if you have the necessary permissions.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-preview-container space-y-4">
|
||||
{!loading && !error && content === 'image' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
|
||||
{fileType && (
|
||||
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
|
||||
)}
|
||||
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
|
||||
{fileType && (
|
||||
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-sm btn-ghost gap-2 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="preview-content overflow-auto border border-base-300 rounded-lg bg-base-200/50 relative">
|
||||
{loading && (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center p-8 bg-base-200 rounded-lg text-center space-y-4">
|
||||
<div className="bg-warning/20 p-4 rounded-full">
|
||||
<Icon icon="mdi:alert" className="h-12 w-12 text-warning" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Preview Unavailable</h3>
|
||||
<p className="text-base-content/70 max-w-md">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-sm btn-ghost gap-2 whitespace-nowrap"
|
||||
className="btn btn-warning btn-sm gap-2 mt-4"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download
|
||||
Download File Instead
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline btn-error mt-4"
|
||||
onClick={loadContent}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && content === 'image' && (
|
||||
<div className="flex justify-center bg-base-200 p-4 rounded-b-lg">
|
||||
<img
|
||||
src={url}
|
||||
alt={filename}
|
||||
className="max-w-full h-auto rounded-lg"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
console.error('Image failed to load:', e);
|
||||
setError('Failed to load image. This might be due to permission issues or the file may not exist.');
|
||||
|
||||
// Log additional details
|
||||
console.log('Image URL that failed:', url);
|
||||
console.log('Current auth status:',
|
||||
Authentication.getInstance().isAuthenticated() ? 'Authenticated' : 'Not authenticated'
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!loading && !error && content === 'video' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
|
||||
{fileType && (
|
||||
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-sm btn-ghost gap-2 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
{!loading && !error && content === 'video' && (
|
||||
<div className="flex justify-center bg-base-200 p-4 rounded-b-lg">
|
||||
<video
|
||||
controls
|
||||
className="max-w-full rounded-lg"
|
||||
style={{ maxHeight: '600px' }}
|
||||
className="max-w-full h-auto rounded-lg"
|
||||
preload="metadata"
|
||||
>
|
||||
<source src={url} type="video/mp4" />
|
||||
<source src={url} type={fileType || 'video/mp4'} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!loading && !error && content === 'pdf' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
|
||||
{fileType && (
|
||||
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-sm btn-ghost gap-2 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
{!loading && !error && content === 'pdf' && (
|
||||
<div className="w-full h-[600px] bg-base-200 p-4 rounded-b-lg">
|
||||
<iframe
|
||||
src={url}
|
||||
|
@ -383,81 +474,30 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
|
|||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!loading && !error && content && !['image', 'video', 'pdf'].includes(content) && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center bg-base-200 p-3 rounded-t-lg">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="truncate font-medium" title={filename}>{truncatedFilename}</span>
|
||||
{fileType && (
|
||||
<span className="badge badge-sm whitespace-nowrap">{fileType.split('/')[1]}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{content && content.split('\n').length > visibleLines && (
|
||||
<button
|
||||
onClick={handleShowMore}
|
||||
className="btn btn-sm btn-ghost"
|
||||
>
|
||||
Show More
|
||||
</button>
|
||||
)}
|
||||
{visibleLines > INITIAL_LINES_TO_SHOW && (
|
||||
<button
|
||||
onClick={handleShowLess}
|
||||
className="btn btn-sm btn-ghost"
|
||||
>
|
||||
Show Less
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-sm btn-ghost gap-2 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{!loading && !error && content && !['image', 'video', 'pdf'].includes(content) && (
|
||||
<div className="overflow-x-auto max-h-[600px] bg-base-200">
|
||||
<div className={`p-1 ${filename.toLowerCase().endsWith('.csv') ? 'p-4' : ''}`}>
|
||||
<div
|
||||
className={filename.toLowerCase().endsWith('.csv') ? '' : 'hljs table w-full font-mono text-sm rounded-lg py-4 px-2'}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatCodeWithLineNumbers(content, getLanguageFromFilename(filename))
|
||||
}}
|
||||
/>
|
||||
{filename.toLowerCase().endsWith('.csv') ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: renderCSVTable(content) }} />
|
||||
) : (
|
||||
<pre className="text-sm">
|
||||
<code
|
||||
className={`language-${getLanguageFromFilename(filename)}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatCodeWithLineNumbers(
|
||||
content.split('\n').slice(0, visibleLines).join('\n'),
|
||||
getLanguageFromFilename(filename)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<span className="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center p-8 bg-base-200 rounded-lg text-center space-y-4">
|
||||
<div className="bg-warning/20 p-4 rounded-full">
|
||||
<Icon icon="mdi:alert" className="h-12 w-12 text-warning" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Preview Unavailable</h3>
|
||||
<p className="text-base-content/70 max-w-md">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="btn btn-warning btn-sm gap-2 mt-4"
|
||||
>
|
||||
<Icon icon="mdi:download" className="h-4 w-4" />
|
||||
Download File Instead
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -30,7 +30,7 @@ export class FileManager {
|
|||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string,
|
||||
file: File
|
||||
file: File,
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to upload files");
|
||||
|
@ -42,7 +42,9 @@ export class FileManager {
|
|||
const formData = new FormData();
|
||||
formData.append(field, file);
|
||||
|
||||
const result = await pb.collection(collectionName).update<T>(recordId, formData);
|
||||
const result = await pb
|
||||
.collection(collectionName)
|
||||
.update<T>(recordId, formData);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`Failed to upload file to ${collectionName}:`, err);
|
||||
|
@ -64,7 +66,7 @@ export class FileManager {
|
|||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string,
|
||||
files: File[]
|
||||
files: File[],
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to upload files");
|
||||
|
@ -73,14 +75,16 @@ export class FileManager {
|
|||
try {
|
||||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file limit
|
||||
const MAX_BATCH_SIZE = 25 * 1024 * 1024; // 25MB per batch
|
||||
|
||||
|
||||
// Validate file sizes first
|
||||
for (const file of files) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
throw new Error(`File ${file.name} is too large. Maximum size is 50MB.`);
|
||||
throw new Error(
|
||||
`File ${file.name} is too large. Maximum size is 50MB.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,10 +92,12 @@ export class FileManager {
|
|||
let existingFiles: string[] = [];
|
||||
if (recordId) {
|
||||
try {
|
||||
const record = await pb.collection(collectionName).getOne<T>(recordId);
|
||||
const record = await pb
|
||||
.collection(collectionName)
|
||||
.getOne<T>(recordId);
|
||||
existingFiles = (record as any)[field] || [];
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch existing record:', error);
|
||||
console.warn("Failed to fetch existing record:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,10 +109,10 @@ export class FileManager {
|
|||
// Process each file
|
||||
for (const file of files) {
|
||||
let processedFile = file;
|
||||
|
||||
|
||||
try {
|
||||
// Try to compress image files if needed
|
||||
if (file.type.startsWith('image/')) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
processedFile = await this.compressImageIfNeeded(file, 50); // 50MB max size
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -118,7 +124,12 @@ export class FileManager {
|
|||
if (currentBatchSize + processedFile.size > MAX_BATCH_SIZE) {
|
||||
// Upload current batch
|
||||
if (currentBatch.length > 0) {
|
||||
await this.uploadBatch(collectionName, recordId, field, currentBatch);
|
||||
await this.uploadBatch(
|
||||
collectionName,
|
||||
recordId,
|
||||
field,
|
||||
currentBatch,
|
||||
);
|
||||
allProcessedFiles.push(...currentBatch);
|
||||
}
|
||||
// Reset batch
|
||||
|
@ -138,7 +149,9 @@ export class FileManager {
|
|||
}
|
||||
|
||||
// Get the final record state
|
||||
const finalRecord = await pb.collection(collectionName).getOne<T>(recordId);
|
||||
const finalRecord = await pb
|
||||
.collection(collectionName)
|
||||
.getOne<T>(recordId);
|
||||
return finalRecord;
|
||||
} catch (err) {
|
||||
console.error(`Failed to upload files to ${collectionName}:`, err);
|
||||
|
@ -156,7 +169,7 @@ export class FileManager {
|
|||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string,
|
||||
files: File[]
|
||||
files: File[],
|
||||
): Promise<void> {
|
||||
const pb = this.auth.getPocketBase();
|
||||
const formData = new FormData();
|
||||
|
@ -170,7 +183,9 @@ export class FileManager {
|
|||
await pb.collection(collectionName).update(recordId, formData);
|
||||
} catch (error: any) {
|
||||
if (error.status === 413) {
|
||||
throw new Error(`Upload failed: Batch size too large. Please try uploading smaller files.`);
|
||||
throw new Error(
|
||||
`Upload failed: Batch size too large. Please try uploading smaller files.`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
@ -188,7 +203,7 @@ export class FileManager {
|
|||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string,
|
||||
files: File[]
|
||||
files: File[],
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to upload files");
|
||||
|
@ -197,10 +212,10 @@ export class FileManager {
|
|||
try {
|
||||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
|
||||
// First, get the current record to check existing files
|
||||
const record = await pb.collection(collectionName).getOne<T>(recordId);
|
||||
|
||||
|
||||
// Create FormData with existing files
|
||||
const formData = new FormData();
|
||||
|
||||
|
@ -210,7 +225,9 @@ export class FileManager {
|
|||
// For each existing file, we need to fetch it and add it to the FormData
|
||||
for (const existingFile of existingFiles) {
|
||||
try {
|
||||
const response = await fetch(this.getFileUrl(collectionName, recordId, existingFile));
|
||||
const response = await fetch(
|
||||
this.getFileUrl(collectionName, recordId, existingFile),
|
||||
);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], existingFile, { type: blob.type });
|
||||
formData.append(field, file);
|
||||
|
@ -218,13 +235,15 @@ export class FileManager {
|
|||
console.warn(`Failed to fetch existing file ${existingFile}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Append new files
|
||||
files.forEach(file => {
|
||||
files.forEach((file) => {
|
||||
formData.append(field, file);
|
||||
});
|
||||
|
||||
const result = await pb.collection(collectionName).update<T>(recordId, formData);
|
||||
const result = await pb
|
||||
.collection(collectionName)
|
||||
.update<T>(recordId, formData);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`Failed to append files to ${collectionName}:`, err);
|
||||
|
@ -244,12 +263,13 @@ export class FileManager {
|
|||
public getFileUrl(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
filename: string
|
||||
filename: string,
|
||||
): string {
|
||||
const pb = this.auth.getPocketBase();
|
||||
const token = pb.authStore.token;
|
||||
const url = `${pb.baseUrl}/api/files/${collectionName}/${recordId}/${filename}`;
|
||||
return url;
|
||||
return pb.files.getURL(
|
||||
{ id: recordId, collectionId: collectionName },
|
||||
filename,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -262,7 +282,7 @@ export class FileManager {
|
|||
public async deleteFile<T = any>(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string
|
||||
field: string,
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to delete files");
|
||||
|
@ -272,7 +292,9 @@ export class FileManager {
|
|||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
const data = { [field]: null };
|
||||
const result = await pb.collection(collectionName).update<T>(recordId, data);
|
||||
const result = await pb
|
||||
.collection(collectionName)
|
||||
.update<T>(recordId, data);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete file from ${collectionName}:`, err);
|
||||
|
@ -292,7 +314,7 @@ export class FileManager {
|
|||
public async downloadFile(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
filename: string
|
||||
filename: string,
|
||||
): Promise<Blob> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to download files");
|
||||
|
@ -302,11 +324,11 @@ export class FileManager {
|
|||
this.auth.setUpdating(true);
|
||||
const url = this.getFileUrl(collectionName, recordId, filename);
|
||||
const response = await fetch(url);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const result = await response.blob();
|
||||
return result;
|
||||
} catch (err) {
|
||||
|
@ -327,7 +349,7 @@ export class FileManager {
|
|||
public async getFiles(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
field: string
|
||||
field: string,
|
||||
): Promise<string[]> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to get files");
|
||||
|
@ -336,18 +358,18 @@ export class FileManager {
|
|||
try {
|
||||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
|
||||
// Get the record to retrieve the filenames
|
||||
const record = await pb.collection(collectionName).getOne(recordId);
|
||||
|
||||
|
||||
// Get the filenames from the specified field
|
||||
const filenames = record[field] || [];
|
||||
|
||||
|
||||
// Convert filenames to URLs
|
||||
const fileUrls = filenames.map((filename: string) =>
|
||||
this.getFileUrl(collectionName, recordId, filename)
|
||||
const fileUrls = filenames.map((filename: string) =>
|
||||
this.getFileUrl(collectionName, recordId, filename),
|
||||
);
|
||||
|
||||
|
||||
return fileUrls;
|
||||
} catch (err) {
|
||||
console.error(`Failed to get files from ${collectionName}:`, err);
|
||||
|
@ -363,8 +385,11 @@ export class FileManager {
|
|||
* @param maxSizeInMB Maximum size in MB
|
||||
* @returns Promise<File> The compressed file
|
||||
*/
|
||||
public async compressImageIfNeeded(file: File, maxSizeInMB: number = 50): Promise<File> {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
public async compressImageIfNeeded(
|
||||
file: File,
|
||||
maxSizeInMB: number = 50,
|
||||
): Promise<File> {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return file;
|
||||
}
|
||||
|
||||
|
@ -379,12 +404,12 @@ export class FileManager {
|
|||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.src = e.target?.result as string;
|
||||
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = document.createElement("canvas");
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
|
||||
// Calculate new dimensions while maintaining aspect ratio
|
||||
const maxDimension = 3840; // Higher quality for larger files
|
||||
if (width > height && width > maxDimension) {
|
||||
|
@ -397,35 +422,155 @@ export class FileManager {
|
|||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0, width, height);
|
||||
|
||||
|
||||
// Convert to blob with higher quality for larger files
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error('Failed to compress image'));
|
||||
reject(new Error("Failed to compress image"));
|
||||
return;
|
||||
}
|
||||
resolve(new File([blob], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now(),
|
||||
}));
|
||||
resolve(
|
||||
new File([blob], file.name, {
|
||||
type: "image/jpeg",
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
);
|
||||
},
|
||||
'image/jpeg',
|
||||
0.85 // Higher quality setting for larger files
|
||||
"image/jpeg",
|
||||
0.85, // Higher quality setting for larger files
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load image for compression'));
|
||||
reject(new Error("Failed to load image for compression"));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Failed to read file for compression'));
|
||||
reject(new Error("Failed to read file for compression"));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file token for accessing protected files
|
||||
* @returns Promise<string> The file token
|
||||
*/
|
||||
public async getFileToken(): Promise<string> {
|
||||
// Check authentication status
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
console.warn("User is not authenticated when trying to get file token");
|
||||
|
||||
// Try to refresh the auth if possible
|
||||
try {
|
||||
const pb = this.auth.getPocketBase();
|
||||
if (pb.authStore.isValid) {
|
||||
console.log(
|
||||
"Auth store is valid, but auth check failed. Trying to refresh token.",
|
||||
);
|
||||
await pb.collection("users").authRefresh();
|
||||
console.log("Auth refreshed successfully");
|
||||
} else {
|
||||
throw new Error("User must be authenticated to get a file token");
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("Failed to refresh authentication:", refreshError);
|
||||
throw new Error("User must be authenticated to get a file token");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
// Log auth status
|
||||
console.log("Auth status before getting token:", {
|
||||
isValid: pb.authStore.isValid,
|
||||
token: pb.authStore.token
|
||||
? pb.authStore.token.substring(0, 10) + "..."
|
||||
: "none",
|
||||
model: pb.authStore.model ? pb.authStore.model.id : "none",
|
||||
});
|
||||
|
||||
const result = await pb.files.getToken();
|
||||
console.log("Got file token:", result.substring(0, 10) + "...");
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error("Failed to get file token:", err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.auth.setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file URL with an optional token for protected files
|
||||
* @param collectionName The name of the collection
|
||||
* @param recordId The ID of the record containing the file
|
||||
* @param filename The name of the file
|
||||
* @param useToken Whether to include a token for protected files
|
||||
* @returns Promise<string> The file URL with token if requested
|
||||
*/
|
||||
public async getFileUrlWithToken(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
filename: string,
|
||||
useToken: boolean = false,
|
||||
): Promise<string> {
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
// Check if filename is empty
|
||||
if (!filename) {
|
||||
console.error(
|
||||
`Empty filename provided for ${collectionName}/${recordId}`,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
console.warn("User is not authenticated when trying to get file URL");
|
||||
}
|
||||
|
||||
// Always try to use token for protected files
|
||||
if (useToken) {
|
||||
try {
|
||||
console.log(
|
||||
`Getting file token for ${collectionName}/${recordId}/${filename}`,
|
||||
);
|
||||
const token = await this.getFileToken();
|
||||
console.log(`Got token: ${token.substring(0, 10)}...`);
|
||||
|
||||
// Make sure to pass the token as a query parameter
|
||||
const url = pb.files.getURL(
|
||||
{ id: recordId, collectionId: collectionName },
|
||||
filename,
|
||||
{ token },
|
||||
);
|
||||
console.log(`Generated URL with token: ${url.substring(0, 50)}...`);
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error("Error getting file token:", error);
|
||||
// Fall back to URL without token
|
||||
const url = pb.files.getURL(
|
||||
{ id: recordId, collectionId: collectionName },
|
||||
filename,
|
||||
);
|
||||
console.log(`Fallback URL without token: ${url.substring(0, 50)}...`);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// If not using token
|
||||
const url = pb.files.getURL(
|
||||
{ id: recordId, collectionId: collectionName },
|
||||
filename,
|
||||
);
|
||||
console.log(`Generated URL without token: ${url.substring(0, 50)}...`);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue