import { useState, useEffect, useRef } from 'react'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { X, Save, Tag, Camera, Trash2, Loader2, Plus, FolderKanban, Hash, Link } from 'lucide-react'; import { api } from '../api/client'; import type { Archive } from '../api/client'; import { Button } from './Button'; const FAILURE_REASONS = [ 'Adhesion failure', 'Spaghetti / Detached', 'Layer shift', 'Clogged nozzle', 'Filament runout', 'Warping', 'Stringing', 'Under-extrusion', 'Power failure', 'User cancelled', 'Other', ]; const ARCHIVE_STATUSES = [ { value: 'completed', label: 'Completed' }, { value: 'failed', label: 'Failed' }, { value: 'aborted', label: 'Cancelled' }, { value: 'printing', label: 'Printing' }, ]; interface EditArchiveModalProps { archive: Archive; onClose: () => void; existingTags?: string[]; } export function EditArchiveModal({ archive, onClose, existingTags = [] }: EditArchiveModalProps) { // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); const queryClient = useQueryClient(); const [printName, setPrintName] = useState(archive.print_name || ''); const [printerId, setPrinterId] = useState(archive.printer_id); const [projectId, setProjectId] = useState(archive.project_id ?? null); const [notes, setNotes] = useState(archive.notes || ''); const [tags, setTags] = useState(archive.tags || ''); const [failureReason, setFailureReason] = useState(archive.failure_reason || ''); const [status, setStatus] = useState(archive.status); const [quantity, setQuantity] = useState(archive.quantity ?? 1); const [photos, setPhotos] = useState(archive.photos || []); const [externalUrl, setExternalUrl] = useState(archive.external_url || ''); const [uploadingPhoto, setUploadingPhoto] = useState(false); const [showTagSuggestions, setShowTagSuggestions] = useState(false); const tagInputRef = useRef(null); const photoInputRef = useRef(null); const blurTimeoutRef = useRef(null); const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); const { data: projects } = useQuery({ queryKey: ['projects'], queryFn: () => api.getProjects(), }); // Get all archives to extract existing tags if not provided const { data: archives } = useQuery({ queryKey: ['archives'], queryFn: () => api.getArchives(undefined, 1000, 0), enabled: existingTags.length === 0, }); // Extract unique tags from all archives const allTags = existingTags.length > 0 ? existingTags : [...new Set( archives?.flatMap(a => a.tags?.split(',').map(t => t.trim()) || []).filter(Boolean) || [] )].sort(); // Get current tags as array const currentTags = tags.split(',').map(t => t.trim()).filter(Boolean); // Filter suggestions based on what's not already added const tagSuggestions = allTags.filter(t => !currentTags.includes(t)); // Add a tag const addTag = (tag: string) => { if (!currentTags.includes(tag)) { const newTags = [...currentTags, tag].join(', '); setTags(newTags); } // Clear any pending blur timeout to prevent hiding suggestions if (blurTimeoutRef.current !== null) { clearTimeout(blurTimeoutRef.current); } tagInputRef.current?.focus(); }; // Remove a tag const removeTag = (tagToRemove: string) => { const newTags = currentTags.filter(t => t !== tagToRemove).join(', '); setTags(newTags); }; const updateMutation = useMutation({ mutationFn: (data: Parameters[1]) => api.updateArchive(archive.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['archives'] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); onClose(); }, }); const handlePhotoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploadingPhoto(true); try { const result = await api.uploadArchivePhoto(archive.id, file); setPhotos(result.photos); queryClient.invalidateQueries({ queryKey: ['archives'] }); } catch (error) { console.error('Failed to upload photo:', error); } finally { setUploadingPhoto(false); if (photoInputRef.current) { photoInputRef.current.value = ''; } } }; const handlePhotoDelete = async (filename: string) => { try { const result = await api.deleteArchivePhoto(archive.id, filename); setPhotos(result.photos || []); queryClient.invalidateQueries({ queryKey: ['archives'] }); } catch (error) { console.error('Failed to delete photo:', error); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Build update data const updateData: Parameters[1] = { print_name: printName || undefined, printer_id: printerId, project_id: projectId, notes: notes || undefined, tags: tags || undefined, quantity: quantity, external_url: externalUrl || null, }; // Only include status if changed if (status !== archive.status) { updateData.status = status; } // Handle failure_reason based on status if (status === 'failed' || status === 'aborted') { updateData.failure_reason = failureReason || undefined; } else if (archive.status === 'failed' || archive.status === 'aborted') { // Clear failure_reason when changing from failed/aborted to another status updateData.failure_reason = null; } updateMutation.mutate(updateData); }; return (
e.stopPropagation()} > {/* Header */}

Edit Archive

{/* Form */}
{/* Print Name */}
setPrintName(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" placeholder="Print name" />
{/* Printer */}
{/* Project */}
{/* Quantity - number of items printed */}
setQuantity(Math.max(1, parseInt(e.target.value) || 1))} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" placeholder="1" />

Number of items produced in this print job

{/* Notes */}