fix event files
This commit is contained in:
parent
4fcb98ba57
commit
94b5e39c1a
3 changed files with 188 additions and 193 deletions
|
@ -744,10 +744,8 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
|||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
// Create updated event object
|
||||
const updatedEvent: Event = {
|
||||
const updatedEvent: Omit<Event, 'created' | 'updated'> = {
|
||||
id: event.id,
|
||||
created: event.created,
|
||||
updated: event.updated,
|
||||
event_name: formData.get("editEventName") as string,
|
||||
event_description: formData.get("editEventDescription") as string,
|
||||
event_code: formData.get("editEventCode") as string,
|
||||
|
@ -764,116 +762,63 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
|||
await services.sendLog.send(
|
||||
"update",
|
||||
"event",
|
||||
`Updating event: ${updatedEvent.event_name} (${updatedEvent.id})`
|
||||
`${event.id ? "Updating" : "Creating"} event: ${updatedEvent.event_name} (${event.id || "new"})`
|
||||
);
|
||||
|
||||
// Process file changes
|
||||
const uploadQueue = new UploadQueue();
|
||||
const fileChanges: FileChanges = {
|
||||
added: selectedFiles,
|
||||
deleted: filesToDelete,
|
||||
unchanged: event.files?.filter(file => !filesToDelete.has(file)) || []
|
||||
};
|
||||
|
||||
// Handle file deletions
|
||||
if (fileChanges.deleted.size > 0) {
|
||||
for (const fileId of fileChanges.deleted) {
|
||||
await services.fileManager.deleteFile("events", event.id, fileId);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 () => {
|
||||
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
|
||||
updatedEvent.files = fileChanges.unchanged;
|
||||
|
||||
// Save the event
|
||||
let savedEvent;
|
||||
try {
|
||||
if (event.id) {
|
||||
// Update existing event
|
||||
savedEvent = await services.update.updateFields<Event>(
|
||||
// We're updating an existing event
|
||||
|
||||
// First, update the event data without touching files
|
||||
const { files, ...cleanPayload } = updatedEvent;
|
||||
await services.update.updateFields<Event>(
|
||||
Collections.EVENTS,
|
||||
event.id,
|
||||
updatedEvent
|
||||
cleanPayload
|
||||
);
|
||||
|
||||
// Handle file operations
|
||||
if (filesToDelete.size > 0 || selectedFiles.size > 0) {
|
||||
// Get the current event with its files
|
||||
const currentEvent = await services.get.getOne<Event>(Collections.EVENTS, event.id);
|
||||
let currentFiles = currentEvent?.files || [];
|
||||
|
||||
// 1. Remove files marked for deletion
|
||||
if (filesToDelete.size > 0) {
|
||||
console.log(`Removing ${filesToDelete.size} files from event ${event.id}`);
|
||||
currentFiles = currentFiles.filter(file => !filesToDelete.has(file));
|
||||
|
||||
// Update the files field first to remove deleted files
|
||||
await services.update.updateFields<Event>(
|
||||
Collections.EVENTS,
|
||||
event.id,
|
||||
{ files: currentFiles }
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Add new files one by one to preserve existing ones
|
||||
if (selectedFiles.size > 0) {
|
||||
console.log(`Adding ${selectedFiles.size} new files to event ${event.id}`);
|
||||
|
||||
// Convert Map to array of File objects
|
||||
const newFiles = Array.from(selectedFiles.values());
|
||||
|
||||
// Use FileManager to upload each file individually
|
||||
for (const file of newFiles) {
|
||||
// Use the FileManager to upload this file
|
||||
await services.fileManager.uploadFile(
|
||||
Collections.EVENTS,
|
||||
event.id,
|
||||
'files',
|
||||
file,
|
||||
true // Set append mode to true to preserve existing files
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the final updated event with all changes
|
||||
const savedEvent = await services.get.getOne<Event>(Collections.EVENTS, event.id);
|
||||
|
||||
// Clear cache to ensure fresh data
|
||||
const dataSync = DataSyncService.getInstance();
|
||||
await dataSync.clearCache();
|
||||
|
@ -885,66 +830,67 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
|||
}
|
||||
|
||||
toast.success("Event updated successfully!");
|
||||
|
||||
// Call the onEventSaved callback if provided
|
||||
if (onEventSaved) onEventSaved();
|
||||
|
||||
// Close the modal
|
||||
handleModalClose();
|
||||
|
||||
} else {
|
||||
// Create new event
|
||||
savedEvent = await services.update.create<Event>(
|
||||
// We're creating a new event
|
||||
|
||||
// Create the event first without files
|
||||
const { files, ...cleanPayload } = updatedEvent;
|
||||
const newEvent = await services.update.create<Event>(
|
||||
Collections.EVENTS,
|
||||
updatedEvent
|
||||
cleanPayload
|
||||
);
|
||||
|
||||
// Log success
|
||||
await services.sendLog.send(
|
||||
"success",
|
||||
"event_create",
|
||||
`Successfully created event: ${savedEvent.event_name}`
|
||||
// Then upload files if any
|
||||
if (selectedFiles.size > 0 && newEvent?.id) {
|
||||
console.log(`Adding ${selectedFiles.size} files to new event ${newEvent.id}`);
|
||||
|
||||
// Convert Map to array of File objects
|
||||
const newFiles = Array.from(selectedFiles.values());
|
||||
|
||||
// Upload files to the new event
|
||||
for (const file of newFiles) {
|
||||
await services.fileManager.uploadFile(
|
||||
Collections.EVENTS,
|
||||
newEvent.id,
|
||||
'files',
|
||||
file,
|
||||
true // Set append mode to true
|
||||
);
|
||||
|
||||
// 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);
|
||||
// Clear cache to ensure fresh data
|
||||
const dataSync = DataSyncService.getInstance();
|
||||
await dataSync.clearCache();
|
||||
|
||||
// Close modal
|
||||
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
|
||||
if (modal) modal.close();
|
||||
toast.success("Event created successfully!");
|
||||
|
||||
// Refresh events list
|
||||
if (window.fetchEvents) {
|
||||
window.fetchEvents();
|
||||
// Call the onEventSaved callback if provided
|
||||
if (onEventSaved) onEventSaved();
|
||||
|
||||
// Close the modal
|
||||
handleModalClose();
|
||||
}
|
||||
|
||||
// Trigger callback
|
||||
if (onEventSaved) {
|
||||
onEventSaved();
|
||||
}
|
||||
// Refresh events list if available
|
||||
if (window.fetchEvents) window.fetchEvents();
|
||||
|
||||
} 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'}`);
|
||||
}
|
||||
toast.error(`Failed to save event: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
window.hideLoading?.();
|
||||
}
|
||||
}, [event, selectedFiles, filesToDelete, services, onEventSaved, isSubmitting]);
|
||||
}, [event, selectedFiles, filesToDelete, services, onEventSaved, isSubmitting, handleModalClose]);
|
||||
|
||||
|
||||
return (
|
||||
<dialog id="editEventModal" className="modal">
|
||||
|
|
|
@ -43,6 +43,7 @@ export class FileManager {
|
|||
* @param recordId The ID of the record to attach the file to
|
||||
* @param field The field name for the file
|
||||
* @param file The file to upload
|
||||
* @param append Whether to append the file to existing files (default: false)
|
||||
* @returns The updated record
|
||||
*/
|
||||
public async uploadFile<T = any>(
|
||||
|
@ -50,6 +51,7 @@ export class FileManager {
|
|||
recordId: string,
|
||||
field: string,
|
||||
file: File,
|
||||
append: boolean = false
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to upload files");
|
||||
|
@ -82,9 +84,16 @@ export class FileManager {
|
|||
extension: fileExtension,
|
||||
collection: collectionName,
|
||||
recordId: recordId,
|
||||
field: field
|
||||
field: field,
|
||||
append: append
|
||||
});
|
||||
|
||||
// Create FormData for the upload
|
||||
const formData = new FormData();
|
||||
|
||||
// Use the + prefix for the field name if append is true
|
||||
const fieldName = append ? `${field}+` : field;
|
||||
|
||||
// Get existing record to preserve existing files
|
||||
let existingRecord: any = null;
|
||||
let existingFiles: string[] = [];
|
||||
|
@ -98,9 +107,13 @@ export class FileManager {
|
|||
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)) {
|
||||
// Check if the file already exists
|
||||
const fileExists = existingFiles.some(existingFile =>
|
||||
existingFile.toLowerCase() === file.name.toLowerCase()
|
||||
);
|
||||
|
||||
if (fileExists) {
|
||||
console.warn(`File with name ${file.name} already exists. Renaming to avoid conflicts.`);
|
||||
const timestamp = new Date().getTime();
|
||||
const nameParts = file.name.split('.');
|
||||
const extension = nameParts.pop();
|
||||
|
@ -108,31 +121,10 @@ export class FileManager {
|
|||
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
|
||||
}
|
||||
const newFile = new File([file], newFileName, { type: file.type });
|
||||
formData.append(fieldName, newFile);
|
||||
} 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
|
||||
}
|
||||
formData.append(fieldName, file);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -140,9 +132,9 @@ export class FileManager {
|
|||
console.log('Upload successful:', {
|
||||
result,
|
||||
fileInfo: {
|
||||
name: fileToUpload.name,
|
||||
size: fileToUpload.size,
|
||||
type: fileToUpload.type
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
},
|
||||
collection: collectionName,
|
||||
recordId: recordId
|
||||
|
|
|
@ -256,4 +256,61 @@ export class Update {
|
|||
this.auth.setUpdating(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a record with file appends
|
||||
* This method properly handles appending files to existing records
|
||||
* @param collectionName The name of the collection
|
||||
* @param recordId The ID of the record to update
|
||||
* @param data Regular fields to update
|
||||
* @param files Object mapping field names to arrays of files to append
|
||||
* @returns The updated record
|
||||
*/
|
||||
public async updateWithFileAppends<T = any>(
|
||||
collectionName: string,
|
||||
recordId: string,
|
||||
data: Record<string, any> = {},
|
||||
files: Record<string, File[]> = {}
|
||||
): Promise<T> {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
throw new Error("User must be authenticated to update records");
|
||||
}
|
||||
|
||||
try {
|
||||
this.auth.setUpdating(true);
|
||||
const pb = this.auth.getPocketBase();
|
||||
|
||||
// Convert regular data fields
|
||||
const convertedData = convertLocalToUTC(data);
|
||||
|
||||
// Create FormData for the update
|
||||
const formData = new FormData();
|
||||
|
||||
// Add regular fields
|
||||
Object.entries(convertedData).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formData.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// Add files with the + prefix to append them
|
||||
Object.entries(files).forEach(([fieldName, fieldFiles]) => {
|
||||
fieldFiles.forEach(file => {
|
||||
formData.append(`${fieldName}+`, file);
|
||||
});
|
||||
});
|
||||
|
||||
// Perform the update
|
||||
const result = await pb
|
||||
.collection(collectionName)
|
||||
.update<T>(recordId, formData);
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`Failed to update with file appends in ${collectionName}:`, err);
|
||||
throw err;
|
||||
} finally {
|
||||
this.auth.setUpdating(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue