fix bugs relating to the event editor

This commit is contained in:
chark1es 2025-03-04 15:55:04 -08:00
parent addfb479b1
commit 3123f6c00c
5 changed files with 539 additions and 156 deletions

View file

@ -123,21 +123,25 @@ const EventCheckIn = () => {
return;
}
// Check if user is already checked in
// Check if user is already checked in - IMPROVED VALIDATION
const attendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
50, // Increased limit to ensure we catch all possible duplicates
`user="${currentUser.id}" && event="${event.id}"`
);
if (attendees.totalItems > 0) {
const lastCheckIn = new Date(attendees.items[0].time_checked_in);
const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime();
const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60));
await logger.send(
"error",
"event_check_in",
`Check-in failed: Already checked in to event: ${event.event_name}`
`Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)`
);
toast.error("You have already checked in to this event");
toast.error(`You have already checked in to this event (${hoursAgo} hours ago)`);
return;
}
@ -211,21 +215,28 @@ const EventCheckIn = () => {
const userId = currentUser.id;
const eventId = event.id;
// Check if user is already checked in
// Double-check for existing check-ins with improved validation
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
50, // Increased limit to ensure we catch all possible duplicates
`user="${userId}" && event="${eventId}"`
);
const isAlreadyCheckedIn = existingAttendees.totalItems > 0;
if (existingAttendees.totalItems > 0) {
const lastCheckIn = new Date(existingAttendees.items[0].time_checked_in);
const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime();
const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60));
if (isAlreadyCheckedIn) {
throw new Error("You have already checked in to this event");
await logger.send(
"error",
"event_check_in",
`Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)`
);
throw new Error(`You have already checked in to this event (${hoursAgo} hours ago)`);
}
// Create new attendee record
// Create new attendee record with transaction to prevent race conditions
const attendeeData = {
user: userId,
event: eventId,
@ -234,11 +245,9 @@ const EventCheckIn = () => {
points_earned: event.points_to_reward || 0
};
// Create the attendee record using PocketBase's create method
// This will properly use the collection rules defined in PocketBase
try {
// Use the update.create method which calls PocketBase's collection.create method
await update.create(Collections.EVENT_ATTENDEES, attendeeData);
// Create the attendee record in PocketBase
const newAttendee = await update.create(Collections.EVENT_ATTENDEES, attendeeData);
console.log("Successfully created attendance record");
@ -265,36 +274,48 @@ const EventCheckIn = () => {
points: totalPoints
});
// Sync the updated user data
// Ensure local data is in sync with backend
// First sync the new attendance record
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
// Then sync the updated user data to ensure points are correctly reflected locally
await dataSync.syncCollection(Collections.USERS);
// Clear event code from local storage
await dataSync.clearEventCode();
// Log successful check-in
await logger.send(
"info",
"event_check_in",
`Successfully checked in to event: ${event.event_name}`
);
// Show success message with event name and points
const pointsMessage = event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)`
: "";
toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`);
// Close any open modals
const foodModal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
if (foodModal) foodModal.close();
const confirmModal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
if (confirmModal) confirmModal.close();
setCurrentCheckInEvent(null);
setFoodInput("");
} catch (createError: any) {
console.error("Error creating attendance record:", createError);
// Check if this is a duplicate record error
// Check if this is a duplicate record error (race condition handling)
if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") {
throw new Error("You have already checked in to this event");
}
throw createError;
}
// Log successful check-in
await logger.send(
"info",
"event_check_in",
`Successfully checked in to event: ${event.event_name}`
);
// Clear event code from local storage
await dataSync.clearEventCode();
// Show success message with event name and points
const pointsMessage = event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)`
: "";
toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`);
setCurrentCheckInEvent(null);
setFoodInput("");
} catch (error: any) {
console.error("Error completing check-in:", error);
toast.error(error.message || "An error occurred during check-in");
@ -317,7 +338,7 @@ const EventCheckIn = () => {
throw new Error("You must be logged in to check in to events");
}
// Get existing attendees or initialize empty array
// Additional check to prevent duplicate check-ins right before submission
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,

View file

@ -209,9 +209,40 @@ const EventForm = memo(({
onChange={(e) => {
if (e.target.files) {
const newFiles = new Map(selectedFiles);
const rejectedFiles: { name: string, reason: string }[] = [];
const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB
Array.from(e.target.files).forEach(file => {
// Validate file size
if (file.size > MAX_FILE_SIZE) {
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2);
rejectedFiles.push({
name: file.name,
reason: `exceeds size limit (${fileSizeMB}MB > 200MB)`
});
return;
}
// Validate file type
const validation = fileManager.validateFileType(file);
if (!validation.valid) {
rejectedFiles.push({
name: file.name,
reason: validation.reason || 'unsupported file type'
});
return;
}
// Only add valid files
newFiles.set(file.name, file);
});
// Show error for rejected files
if (rejectedFiles.length > 0) {
const errorMessage = `The following files were not added:\n${rejectedFiles.map(f => `${f.name}: ${f.reason}`).join('\n')}`;
toast.error(errorMessage);
}
setSelectedFiles(newFiles);
}
}}
@ -620,7 +651,26 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
try {
if (event?.id) {
await initializeEventData(event.id);
// If we have a complete event object, use it directly
if (event.event_name && event.event_description) {
console.log("Using provided event data:", event);
// Format dates for datetime-local input
if (event.start_date) {
const startDate = new Date(event.start_date);
event.start_date = Get.formatLocalDate(startDate, false);
}
if (event.end_date) {
const endDate = new Date(event.end_date);
event.end_date = Get.formatLocalDate(endDate, false);
}
setEvent(event);
} else {
// Otherwise fetch it from the server
await initializeEventData(event.id);
}
} else {
await initializeEventData('');
}
@ -733,21 +783,82 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
}
}
// Handle file uploads
// Handle file uploads - only upload new files
if (fileChanges.added.size > 0) {
const uploadErrors: string[] = [];
const fileManager = services.fileManager;
// Check for unsupported file types first
const invalidFiles = Array.from(fileChanges.added.entries())
.map(([filename, file]) => {
const validation = fileManager.validateFileType(file);
return { filename, file, validation };
})
.filter(item => !item.validation.valid);
if (invalidFiles.length > 0) {
const errorMessage = `The following files cannot be uploaded:\n${invalidFiles.map(item => `${item.filename}: ${item.validation.reason}`).join('\n')}`;
toast.error(errorMessage);
throw new Error(errorMessage);
}
for (const [filename, file] of fileChanges.added.entries()) {
await uploadQueue.add(async () => {
const uploadedFile = await services.fileManager.uploadFile(
"events",
event.id,
filename,
file
);
if (uploadedFile) {
fileChanges.unchanged.push(uploadedFile);
try {
// Validate file size before compression
const maxSize = 200 * 1024 * 1024; // 200MB
if (file.size > maxSize) {
throw new Error(`File ${filename} exceeds 200MB limit`);
}
// Compress image if it's an image file
const compressedFile = file.type.startsWith('image/')
? await services.fileManager.compressImageIfNeeded(file, 10) // 10MB limit for images
: file;
console.log(`Uploading file ${filename}:`, {
originalSize: file.size,
compressedSize: compressedFile.size,
type: file.type
});
// Upload the file to PocketBase
const uploadedFile = await services.fileManager.uploadFile(
"events",
event.id,
"files", // Use the correct field name from the schema
compressedFile
);
if (uploadedFile && uploadedFile.files) {
// Get the filename from the uploaded file
const uploadedFilename = uploadedFile.files[uploadedFile.files.length - 1];
fileChanges.unchanged.push(uploadedFilename);
console.log(`Successfully uploaded ${filename} as ${uploadedFilename}`);
} else {
console.warn(`File uploaded but no filename returned:`, uploadedFile);
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error('File upload failed:', {
filename,
error: errorMsg,
fileInfo: {
size: file.size,
type: file.type
}
});
uploadErrors.push(`${filename}: ${errorMsg}`);
}
});
}
// If any uploads failed, show error and stop
if (uploadErrors.length > 0) {
const errorMessage = `Failed to upload files:\n${uploadErrors.join('\n')}`;
toast.error(errorMessage);
throw new Error(errorMessage);
}
}
// Update the event with the new file list
@ -755,81 +866,81 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
// Save the event
let savedEvent;
if (event.id) {
// Update existing event
savedEvent = await services.update.updateFields<Event>(
Collections.EVENTS,
event.id,
updatedEvent
);
try {
if (event.id) {
// Update existing event
savedEvent = await services.update.updateFields<Event>(
Collections.EVENTS,
event.id,
updatedEvent
);
// Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance();
await dataSync.clearCache();
// Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance();
await dataSync.clearCache();
// Log success
await services.sendLog.send(
"success",
"event_update",
`Successfully updated event: ${savedEvent.event_name}`
);
// Update the window object with the latest event data
const eventDataId = `event_${event.id}`;
if ((window as any)[eventDataId]) {
(window as any)[eventDataId] = savedEvent;
}
// Show success toast
toast.success(`Event "${savedEvent.event_name}" updated successfully!`);
} else {
// Create new event
savedEvent = await services.update.create<Event>(
Collections.EVENTS,
updatedEvent
);
toast.success("Event updated successfully!");
} else {
// Create new event
savedEvent = await services.update.create<Event>(
Collections.EVENTS,
updatedEvent
);
// Log success
await services.sendLog.send(
"success",
"event_create",
`Successfully created event: ${savedEvent.event_name}`
);
// Log success
await services.sendLog.send(
"success",
"event_create",
`Successfully created event: ${savedEvent.event_name}`
);
// Show success toast
toast.success(`Event "${savedEvent.event_name}" created successfully!`);
// Show success toast
toast.success(`Event "${savedEvent.event_name}" created successfully!`);
}
// Reset form state
setEvent({
id: "",
created: "",
updated: "",
event_name: "",
event_description: "",
event_code: "",
location: "",
files: [],
points_to_reward: 0,
start_date: "",
end_date: "",
published: false,
has_food: false
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setHasUnsavedChanges(false);
// Close modal
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close();
// Refresh events list
if (window.fetchEvents) {
window.fetchEvents();
}
// Trigger callback
if (onEventSaved) {
onEventSaved();
}
} catch (error) {
console.error("Failed to save event:", error);
toast.error(`Failed to ${event.id ? "update" : "create"} event: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Reset form state
setEvent({
id: "",
created: "",
updated: "",
event_name: "",
event_description: "",
event_code: "",
location: "",
files: [],
points_to_reward: 0,
start_date: "",
end_date: "",
published: false,
has_food: false
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setHasUnsavedChanges(false);
// Close modal
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close();
// Refresh events list
if (window.fetchEvents) {
window.fetchEvents();
}
// Trigger callback
if (onEventSaved) {
onEventSaved();
}
} catch (error) {
console.error("Failed to save event:", error);
toast.error(`Failed to ${event.id ? "update" : "create"} event: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSubmitting(false);
window.hideLoading?.();

View file

@ -182,8 +182,14 @@ export class DataSyncService {
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
if (collection === Collections.EVENTS && 'event_code' in item) {
const { event_code, ...rest } = item as any;
item = rest as T;
// Keep the event_code but ensure files array is properly handled
if ('files' in item && Array.isArray((item as any).files)) {
// Ensure files array is properly stored
console.log(`Event ${item.id} has ${(item as any).files.length} files`);
} else {
// Initialize empty files array if not present
(item as any).files = [];
}
}
if (existingItem) {
@ -223,10 +229,23 @@ export class DataSyncService {
localItem: T,
serverItem: T,
): Promise<T> {
// SECURITY FIX: Remove event_code from events before resolving conflicts
if (collection === Collections.EVENTS && 'event_code' in serverItem) {
const { event_code, ...rest } = serverItem as any;
serverItem = rest as T;
// For events, ensure we handle the files field properly
if (collection === Collections.EVENTS) {
// Ensure files array is properly handled
if ('files' in serverItem && Array.isArray((serverItem as any).files)) {
console.log(`Server event ${serverItem.id} has ${(serverItem as any).files.length} files`);
} else {
// Initialize empty files array if not present
(serverItem as any).files = [];
}
// If local item has files but server doesn't, preserve local files
if ('files' in localItem && Array.isArray((localItem as any).files) &&
(localItem as any).files.length > 0 &&
(!('files' in serverItem) || !(serverItem as any).files.length)) {
console.log(`Preserving local files for event ${localItem.id}`);
(serverItem as any).files = (localItem as any).files;
}
}
// Check if there are pending offline changes for this item
@ -248,7 +267,16 @@ export class DataSyncService {
if (change.operation === "update" && change.data) {
// Apply each field change individually
Object.entries(change.data).forEach(([key, value]) => {
(mergedItem as any)[key] = value;
// Special handling for files array
if (key === 'files' && Array.isArray(value)) {
// Merge files arrays, removing duplicates
const existingFiles = Array.isArray((mergedItem as any)[key]) ? (mergedItem as any)[key] : [];
const newFiles = value as string[];
(mergedItem as any)[key] = [...new Set([...existingFiles, ...newFiles])];
console.log(`Merged files for ${collection}:${localItem.id}`, (mergedItem as any)[key]);
} else {
(mergedItem as any)[key] = value;
}
});
}
}
@ -507,11 +535,22 @@ export class DataSyncService {
try {
const pbItem = await this.get.getOne<T>(collection, id);
if (pbItem) {
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
if (collection === Collections.EVENTS && 'event_code' in pbItem) {
const { event_code, ...rest } = pbItem as any;
await table.put(rest as T);
item = rest as T;
// For events, ensure we handle the files field properly
if (collection === Collections.EVENTS) {
// Ensure files array is properly handled
if (!('files' in pbItem) || !Array.isArray((pbItem as any).files)) {
(pbItem as any).files = [];
}
// If we already have a local item with files, preserve them if server has none
if (item && 'files' in item && Array.isArray((item as any).files) &&
(item as any).files.length > 0 && !(pbItem as any).files.length) {
console.log(`Preserving local files for event ${id}`);
(pbItem as any).files = (item as any).files;
}
await table.put(pbItem);
item = pbItem;
} else {
await table.put(pbItem);
item = pbItem;
@ -547,6 +586,25 @@ export class DataSyncService {
return undefined;
}
// Special handling for files field in events
if (collection === Collections.EVENTS && 'files' in data) {
console.log(`Updating files for event ${id}`, (data as any).files);
// Ensure files is an array
if (!Array.isArray((data as any).files)) {
(data as any).files = [];
}
// If we're updating files, make sure we're not losing any
if ('files' in currentItem && Array.isArray((currentItem as any).files)) {
// Merge files arrays, removing duplicates
const existingFiles = (currentItem as any).files as string[];
const newFiles = (data as any).files as string[];
(data as any).files = [...new Set([...existingFiles, ...newFiles])];
console.log(`Merged files for event ${id}`, (data as any).files);
}
}
// Update the item in IndexedDB
const updatedItem = {
...currentItem,

View file

@ -71,6 +71,11 @@ export class DashboardDatabase extends Dexie {
events: "id, event_name, event_code, start_date, end_date, published",
eventAttendees: "id, user, event, time_checked_in",
});
// Add version 4 with files field in events table
this.version(4).stores({
events: "id, event_name, event_code, start_date, end_date, published, files",
});
}
// Initialize the database with default values

View file

@ -3,6 +3,7 @@ import { Authentication } from "./Authentication";
export class FileManager {
private auth: Authentication;
private static instance: FileManager;
private static UNSUPPORTED_EXTENSIONS = ['afdesign', 'psd', 'ai', 'sketch'];
private constructor() {
this.auth = Authentication.getInstance();
@ -18,6 +19,24 @@ export class FileManager {
return FileManager.instance;
}
/**
* Validates if a file type is supported
* @param file The file to validate
* @returns Object with validation result and reason if invalid
*/
public validateFileType(file: File): { valid: boolean; reason?: string } {
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (fileExtension && FileManager.UNSUPPORTED_EXTENSIONS.includes(fileExtension)) {
return {
valid: false,
reason: `File type .${fileExtension} is not supported. Please convert to PDF or image format.`
};
}
return { valid: true };
}
/**
* Upload a single file to a record
* @param collectionName The name of the collection
@ -39,16 +58,142 @@ export class FileManager {
try {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
const formData = new FormData();
formData.append(field, file);
const result = await pb
.collection(collectionName)
.update<T>(recordId, formData);
return result;
// Validate file size
const maxSize = 200 * 1024 * 1024; // 200MB
if (file.size > maxSize) {
throw new Error(`File size ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds 200MB limit`);
}
// Check for potentially problematic file types
const fileExtension = file.name.split('.').pop()?.toLowerCase();
// Validate file type
const validation = this.validateFileType(file);
if (!validation.valid) {
throw new Error(validation.reason);
}
// Log upload attempt
console.log('Attempting file upload:', {
name: file.name,
size: file.size,
type: file.type,
extension: fileExtension,
collection: collectionName,
recordId: recordId,
field: field
});
// Get existing record to preserve existing files
let existingRecord: any = null;
let existingFiles: string[] = [];
try {
if (recordId) {
existingRecord = await pb.collection(collectionName).getOne(recordId);
existingFiles = existingRecord[field] || [];
}
} catch (error) {
console.warn('Could not fetch existing record:', error);
}
// Check if the file already exists in the record
let fileToUpload = file;
if (recordId && existingFiles.includes(file.name)) {
const timestamp = new Date().getTime();
const nameParts = file.name.split('.');
const extension = nameParts.pop();
const baseName = nameParts.join('.');
const newFileName = `${baseName}_${timestamp}.${extension}`;
// Create a new file with the modified name
fileToUpload = new File([file], newFileName, { type: file.type });
console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`);
}
// Create FormData and append file
const formData = new FormData();
// For events collection, use the 'files' field from the schema
if (collectionName === 'events') {
// Only append the new file, don't re-upload existing files
formData.append('files', fileToUpload);
// If this is an update operation and we have existing files, we need to tell PocketBase to keep them
if (recordId && existingFiles.length > 0) {
formData.append('files@', ''); // This tells PocketBase to keep existing files
}
} else {
// For other collections, use the provided field name
formData.append(field, fileToUpload);
// If this is an update operation and we have existing files, we need to tell PocketBase to keep them
if (recordId && existingFiles.length > 0) {
formData.append(`${field}@`, ''); // This tells PocketBase to keep existing files
}
}
try {
const result = await pb.collection(collectionName).update<T>(recordId, formData);
console.log('Upload successful:', {
result,
fileInfo: {
name: fileToUpload.name,
size: fileToUpload.size,
type: fileToUpload.type
},
collection: collectionName,
recordId: recordId
});
// Verify the file was actually added to the record
try {
const updatedRecord = await pb.collection(collectionName).getOne(recordId);
console.log('Updated record files:', {
files: updatedRecord.files,
recordId: recordId
});
} catch (verifyError) {
console.warn('Could not verify file upload:', verifyError);
}
return result;
} catch (pbError: any) {
// Log detailed PocketBase error
console.error('PocketBase upload error:', {
status: pbError?.status,
response: pbError?.response,
data: pbError?.data,
message: pbError?.message
});
// More specific error message based on file type
if (fileExtension && FileManager.UNSUPPORTED_EXTENSIONS.includes(fileExtension)) {
throw new Error(`Upload failed: File type .${fileExtension} is not supported. Please convert to PDF or image format.`);
}
throw new Error(`Upload failed: ${pbError?.message || 'Unknown PocketBase error'}`);
}
} catch (err) {
console.error(`Failed to upload file to ${collectionName}:`, err);
throw err;
console.error(`Failed to upload file to ${collectionName}:`, {
error: err,
fileInfo: {
name: file.name,
size: file.size,
type: file.type
},
auth: {
isAuthenticated: this.auth.isAuthenticated(),
userId: this.auth.getUserId()
}
});
if (err instanceof Error) {
throw err;
}
throw new Error(`Upload failed: ${err}`);
} finally {
this.auth.setUpdating(false);
}
@ -79,13 +224,20 @@ export class FileManager {
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
// Validate file types and sizes first
for (const file of files) {
// Validate file size
if (file.size > MAX_FILE_SIZE) {
throw new Error(
`File ${file.name} is too large. Maximum size is 50MB.`,
);
}
// Validate file type
const validation = this.validateFileType(file);
if (!validation.valid) {
throw new Error(`File ${file.name}: ${validation.reason}`);
}
}
// Get existing record if updating
@ -174,9 +326,39 @@ export class FileManager {
const pb = this.auth.getPocketBase();
const formData = new FormData();
// Add new files
// Get existing files to check for duplicates
let existingFiles: string[] = [];
try {
const record = await pb.collection(collectionName).getOne(recordId);
existingFiles = record[field] || [];
} catch (error) {
console.warn("Failed to fetch existing record for duplicate check:", error);
}
// Add new files, renaming duplicates if needed
for (const file of files) {
formData.append(field, file);
let fileToUpload = file;
// Check if filename already exists
if (Array.isArray(existingFiles) && existingFiles.includes(file.name)) {
const timestamp = new Date().getTime();
const nameParts = file.name.split('.');
const extension = nameParts.pop();
const baseName = nameParts.join('.');
const newFileName = `${baseName}_${timestamp}.${extension}`;
// Create a new file with the modified name
fileToUpload = new File([file], newFileName, { type: file.type });
console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`);
}
formData.append(field, fileToUpload);
}
// Tell PocketBase to keep existing files
if (existingFiles.length > 0) {
formData.append(`${field}@`, ''); // This tells PocketBase to keep existing files
}
try {
@ -216,30 +398,36 @@ export class FileManager {
// 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();
// Get existing files from the record
const existingFiles = (record as any)[field] || [];
const existingFilenames = new Set(existingFiles);
// 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 blob = await response.blob();
const file = new File([blob], existingFile, { type: blob.type });
formData.append(field, file);
} catch (error) {
console.warn(`Failed to fetch existing file ${existingFile}:`, error);
// Create FormData for the new files only
const formData = new FormData();
// Tell PocketBase to keep existing files
formData.append(`${field}@`, '');
// Append new files, renaming if needed to avoid duplicates
for (const file of files) {
let fileToUpload = file;
// Check if filename already exists
if (existingFilenames.has(file.name)) {
const timestamp = new Date().getTime();
const nameParts = file.name.split('.');
const extension = nameParts.pop();
const baseName = nameParts.join('.');
const newFileName = `${baseName}_${timestamp}.${extension}`;
// Create a new file with the modified name
fileToUpload = new File([file], newFileName, { type: file.type });
console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`);
}
}
// Append new files
files.forEach((file) => {
formData.append(field, file);
});
formData.append(field, fileToUpload);
}
const result = await pb
.collection(collectionName)