New Features:

- Print from File Manager with plate selection, AMS mapping, and print options. Closes #94
- Add to queue without creating archive (reduces clutter)
- Queue displays library file name, thumbnail, and print time

Fixes:
Queue items from File Manager now reference library files directly instead of
creating archives upfront. Archives are created automatically when prints start.
This commit is contained in:
maziggy
2026-01-20 15:37:20 +01:00
parent ec66e0a665
commit 02f6de4adc
21 changed files with 1322 additions and 381 deletions
@@ -12,9 +12,7 @@ import type { FilamentMappingProps } from './types';
*/
export function FilamentMapping({
printerId,
archiveId,
selectedPlate,
isMultiPlate,
filamentReqs,
manualMappings,
onManualMappingChange,
}: FilamentMappingProps) {
@@ -22,13 +20,6 @@ export function FilamentMapping({
const [isRefreshing, setIsRefreshing] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
// Fetch filament requirements from the archived 3MF (filtered by plate if selected)
const { data: filamentReqs } = useQuery({
queryKey: ['archive-filaments', archiveId, selectedPlate],
queryFn: () => api.getArchiveFilamentRequirements(archiveId, selectedPlate ?? undefined),
enabled: selectedPlate !== null || !isMultiPlate,
});
// Fetch printer status
const { data: printerStatus } = useQuery({
queryKey: ['printer-status', printerId],
@@ -2,21 +2,19 @@ import { Printer as PrinterIcon, Loader2, AlertCircle, Check } from 'lucide-reac
import type { PrinterSelectorProps } from './types';
/**
* Printer selection component with multiple modes:
* - Grid mode (default): Shows printers as selectable cards (single or multi-select)
* - Dropdown mode: Shows printers in a select dropdown (used when allowUnassigned is true)
* Printer selection component with grid-based UI.
* Supports single or multi-select modes.
*/
export function PrinterSelector({
printers,
selectedPrinterId,
selectedPrinterIds = [],
onSelect,
selectedPrinterIds,
onMultiSelect,
isLoading = false,
allowUnassigned = false,
allowMultiple = false,
showInactive = false,
}: PrinterSelectorProps) {
const activePrinters = printers.filter((p) => p.is_active);
// Filter printers based on showInactive flag
const displayPrinters = showInactive ? printers : printers.filter((p) => p.is_active);
if (isLoading) {
return (
@@ -26,53 +24,17 @@ export function PrinterSelector({
);
}
// Use dropdown mode for edit scenarios (allows unassigning printer)
if (allowUnassigned) {
if (displayPrinters.length === 0) {
return (
<div>
<label className="block text-sm text-bambu-gray mb-1">Printer</label>
{printers.length === 0 ? (
<div className="flex items-center gap-2 text-red-400 text-sm">
<AlertCircle className="w-4 h-4" />
No printers configured
</div>
) : (
<>
<select
className={`w-full px-3 py-2 bg-bambu-dark border rounded-lg text-white focus:border-bambu-green focus:outline-none ${
selectedPrinterId === null ? 'border-orange-400' : 'border-bambu-dark-tertiary'
}`}
value={selectedPrinterId ?? ''}
onChange={(e) => onSelect(e.target.value ? Number(e.target.value) : null)}
>
<option value="">-- Select a printer --</option>
{printers.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
{selectedPrinterId === null && (
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Assign a printer to enable printing
</p>
)}
</>
)}
<div className="flex items-center gap-2 text-red-400 text-sm mb-4">
<AlertCircle className="w-4 h-4" />
No {showInactive ? '' : 'active '}printers available
</div>
);
}
// Grid mode for reprint/add-to-queue (only active printers)
if (activePrinters.length === 0) {
return (
<div className="text-center py-8 text-bambu-gray">No active printers available</div>
);
}
const handlePrinterClick = (printerId: number) => {
if (allowMultiple && onMultiSelect) {
if (allowMultiple) {
// Multi-select mode: toggle printer in selection
if (selectedPrinterIds.includes(printerId)) {
onMultiSelect(selectedPrinterIds.filter((id) => id !== printerId));
@@ -80,36 +42,27 @@ export function PrinterSelector({
onMultiSelect([...selectedPrinterIds, printerId]);
}
} else {
// Single-select mode
onSelect(printerId);
// Single-select mode: replace selection
onMultiSelect([printerId]);
}
};
const handleSelectAll = () => {
if (onMultiSelect) {
onMultiSelect(activePrinters.map((p) => p.id));
}
onMultiSelect(displayPrinters.map((p) => p.id));
};
const handleDeselectAll = () => {
if (onMultiSelect) {
onMultiSelect([]);
}
onMultiSelect([]);
};
const isSelected = (printerId: number) => {
if (allowMultiple) {
return selectedPrinterIds.includes(printerId);
}
return selectedPrinterId === printerId;
};
const isSelected = (printerId: number) => selectedPrinterIds.includes(printerId);
const selectedCount = allowMultiple ? selectedPrinterIds.length : (selectedPrinterId ? 1 : 0);
const selectedCount = selectedPrinterIds.length;
return (
<div className="space-y-2 mb-6">
{/* Multi-select header */}
{allowMultiple && activePrinters.length > 1 && (
{allowMultiple && displayPrinters.length > 1 && (
<div className="flex items-center justify-between text-xs text-bambu-gray mb-2">
<span>
{selectedCount === 0
@@ -117,7 +70,7 @@ export function PrinterSelector({
: `${selectedCount} printer${selectedCount !== 1 ? 's' : ''} selected`}
</span>
<div className="flex gap-2">
{selectedCount < activePrinters.length && (
{selectedCount < displayPrinters.length && (
<button
type="button"
onClick={handleSelectAll}
@@ -139,7 +92,7 @@ export function PrinterSelector({
</div>
)}
{activePrinters.map((printer) => (
{displayPrinters.map((printer) => (
<button
key={printer.id}
type="button"
@@ -148,7 +101,7 @@ export function PrinterSelector({
isSelected(printer.id)
? 'border-bambu-green bg-bambu-green/10'
: 'border-bambu-dark-tertiary bg-bambu-dark hover:border-bambu-gray'
}`}
} ${!printer.is_active ? 'opacity-60' : ''}`}
>
<div
className={`p-2 rounded-lg ${
@@ -162,7 +115,10 @@ export function PrinterSelector({
/>
</div>
<div className="text-left flex-1">
<p className="text-white font-medium">{printer.name}</p>
<p className="text-white font-medium">
{printer.name}
{!printer.is_active && <span className="text-bambu-gray text-xs ml-2">(inactive)</span>}
</p>
<p className="text-xs text-bambu-gray">
{printer.model || 'Unknown model'} • {printer.ip_address}
</p>
@@ -181,6 +137,14 @@ export function PrinterSelector({
)}
</button>
))}
{/* Warning when no printer selected */}
{selectedCount === 0 && (
<p className="text-xs text-orange-400 mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Select at least one printer
</p>
)}
</div>
);
}
+140 -98
View File
@@ -23,13 +23,17 @@ import { DEFAULT_PRINT_OPTIONS, DEFAULT_SCHEDULE_OPTIONS } from './types';
/**
* Unified PrintModal component that handles three modes:
* - 'reprint': Immediate print from archive (supports multi-printer)
* - 'add-to-queue': Schedule print to queue (supports multi-printer)
* - 'edit-queue-item': Edit existing queue item (single printer only)
* - 'reprint': Immediate print from archive or library file (supports multi-printer)
* - 'add-to-queue': Schedule print to queue from archive or library file (supports multi-printer)
* - 'edit-queue-item': Edit existing queue item (supports multi-printer)
*
* Both archiveId and libraryFileId are supported. Library files can be printed immediately
* or added to queue (archive is created at print start time, not when queued).
*/
export function PrintModal({
mode,
archiveId,
libraryFileId,
archiveName,
queueItem,
onClose,
@@ -38,16 +42,17 @@ export function PrintModal({
const queryClient = useQueryClient();
const { showToast } = useToast();
// Single printer selection (for edit mode and backward compatibility)
const [selectedPrinter, setSelectedPrinter] = useState<number | null>(() => {
if (mode === 'edit-queue-item' && queueItem) {
return queueItem.printer_id;
}
return null;
});
// Determine if we're printing a library file
const isLibraryFile = !!libraryFileId && !archiveId;
// Multiple printer selection (for reprint and add-to-queue modes)
const [selectedPrinters, setSelectedPrinters] = useState<number[]>([]);
// Multiple printer selection (used for all modes now)
const [selectedPrinters, setSelectedPrinters] = useState<number[]>(() => {
// Initialize with the queue item's printer if editing
if (mode === 'edit-queue-item' && queueItem?.printer_id) {
return [queueItem.printer_id];
}
return [];
});
const [selectedPlate, setSelectedPlate] = useState<number | null>(() => {
if (mode === 'edit-queue-item' && queueItem) {
@@ -109,20 +114,17 @@ export function PrintModal({
});
// Track initial values for clearing mappings on change (edit mode only)
const [initialPrinterId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.printer_id : null));
const [initialPrinterIds] = useState(() => (mode === 'edit-queue-item' && queueItem?.printer_id ? [queueItem.printer_id] : []));
const [initialPlateId] = useState(() => (mode === 'edit-queue-item' && queueItem ? queueItem.plate_id : null));
// Submission state for multi-printer
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitProgress, setSubmitProgress] = useState({ current: 0, total: 0 });
// Determine if we're in multi-printer mode
const isMultiPrinterMode = mode !== 'edit-queue-item';
const effectivePrinterCount = isMultiPrinterMode ? selectedPrinters.length : (selectedPrinter ? 1 : 0);
// Printer counts and effective printer for filament mapping
const effectivePrinterCount = selectedPrinters.length;
// For filament mapping, use first selected printer (mapping applies to all)
const effectivePrinterId = isMultiPrinterMode
? (selectedPrinters.length > 0 ? selectedPrinters[0] : null)
: selectedPrinter;
const effectivePrinterId = selectedPrinters.length > 0 ? selectedPrinters[0] : null;
// Queries
const { data: printers, isLoading: loadingPrinters } = useQuery({
@@ -130,17 +132,45 @@ export function PrintModal({
queryFn: api.getPrinters,
});
const { data: platesData } = useQuery({
// Fetch plates for archives
const { data: archivePlatesData, isError: archivePlatesError } = useQuery({
queryKey: ['archive-plates', archiveId],
queryFn: () => api.getArchivePlates(archiveId),
queryFn: () => api.getArchivePlates(archiveId!),
enabled: !!archiveId && !isLibraryFile,
retry: false,
});
const { data: filamentReqs } = useQuery({
queryKey: ['archive-filaments', archiveId, selectedPlate],
queryFn: () => api.getArchiveFilamentRequirements(archiveId, selectedPlate ?? undefined),
enabled: selectedPlate !== null || !platesData?.is_multi_plate,
// Fetch plates for library files
const { data: libraryPlatesData } = useQuery({
queryKey: ['library-file-plates', libraryFileId],
queryFn: () => api.getLibraryFilePlates(libraryFileId!),
enabled: isLibraryFile && !!libraryFileId,
});
// Combine plates data from either source
const platesData = isLibraryFile ? libraryPlatesData : archivePlatesData;
// Fetch filament requirements for archives
const { data: archiveFilamentReqs, isError: archiveFilamentReqsError } = useQuery({
queryKey: ['archive-filaments', archiveId, selectedPlate],
queryFn: () => api.getArchiveFilamentRequirements(archiveId!, selectedPlate ?? undefined),
enabled: !!archiveId && !isLibraryFile && (selectedPlate !== null || !platesData?.is_multi_plate),
retry: false,
});
// Fetch filament requirements for library files (with plate support)
const { data: libraryFilamentReqs } = useQuery({
queryKey: ['library-file-filaments', libraryFileId, selectedPlate],
queryFn: () => api.getLibraryFileFilamentRequirements(libraryFileId!, selectedPlate ?? undefined),
enabled: isLibraryFile && !!libraryFileId && (selectedPlate !== null || !platesData?.is_multi_plate),
});
// Track if archive data couldn't be loaded (archive deleted or file missing)
const archiveDataMissing = !isLibraryFile && (archivePlatesError || archiveFilamentReqsError);
// Combine filament requirements from either source
const effectiveFilamentReqs = isLibraryFile ? libraryFilamentReqs : archiveFilamentReqs;
// Only fetch printer status when single printer selected (for filament mapping)
const { data: printerStatus } = useQuery({
queryKey: ['printer-status', effectivePrinterId],
@@ -149,7 +179,7 @@ export function PrintModal({
});
// Get AMS mapping from hook (only when single printer selected)
const { amsMapping } = useFilamentMapping(filamentReqs, printerStatus, manualMappings);
const { amsMapping } = useFilamentMapping(effectiveFilamentReqs, printerStatus, manualMappings);
// Auto-select first plate for single-plate files
useEffect(() => {
@@ -158,8 +188,9 @@ export function PrintModal({
}
}, [platesData, selectedPlate]);
// Auto-select first printer when only one available (non-multi mode)
// Auto-select first printer when only one available
useEffect(() => {
// Skip auto-select for edit mode (already initialized from queueItem)
if (mode === 'edit-queue-item') return;
const activePrinters = printers?.filter(p => p.is_active) || [];
if (activePrinters.length === 1 && selectedPrinters.length === 0) {
@@ -170,13 +201,15 @@ export function PrintModal({
// Clear manual mappings when printer or plate changes
useEffect(() => {
if (mode === 'edit-queue-item') {
if (selectedPrinter !== initialPrinterId || selectedPlate !== initialPlateId) {
// For edit mode, clear mappings if printer selection or plate changed from initial
const printersChanged = JSON.stringify(selectedPrinters.sort()) !== JSON.stringify(initialPrinterIds.sort());
if (printersChanged || selectedPlate !== initialPlateId) {
setManualMappings({});
}
} else {
setManualMappings({});
}
}, [mode, selectedPrinter, selectedPrinters, selectedPlate, initialPrinterId, initialPlateId]);
}, [mode, selectedPrinters, selectedPlate, initialPrinterIds, initialPlateId]);
// Close on Escape key
useEffect(() => {
@@ -212,29 +245,7 @@ export function PrintModal({
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
if (mode === 'edit-queue-item') {
// Edit mode - single printer update
const data: PrintQueueItemUpdate = {
printer_id: selectedPrinter,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: amsMapping,
plate_id: selectedPlate,
...printOptions,
};
if (scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime) {
data.scheduled_time = new Date(scheduleOptions.scheduledTime).toISOString();
} else {
data.scheduled_time = null;
}
updateQueueMutation.mutate(data);
return;
}
// Multi-printer modes (reprint or add-to-queue)
// Validate printer selection
if (selectedPrinters.length === 0) {
showToast('Please select at least one printer', 'error');
return;
@@ -249,37 +260,60 @@ export function PrintModal({
errors: [],
};
// Common queue data for add-to-queue and edit modes
const getQueueData = (printerId: number): PrintQueueItemCreate => ({
printer_id: printerId,
// Use library_file_id for library files, archive_id for archives
archive_id: isLibraryFile ? undefined : archiveId,
library_file_id: isLibraryFile ? libraryFileId : undefined,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: amsMapping,
plate_id: selectedPlate,
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
? new Date(scheduleOptions.scheduledTime).toISOString()
: undefined,
...printOptions,
});
for (let i = 0; i < selectedPrinters.length; i++) {
const printerId = selectedPrinters[i];
setSubmitProgress({ current: i + 1, total: selectedPrinters.length });
try {
// Use the same AMS mapping for all printers (configured via UI based on first printer)
// This assumes all printers have the same filament configuration
if (mode === 'reprint') {
await api.reprintArchive(archiveId, printerId, {
plate_id: selectedPlate ?? undefined,
ams_mapping: amsMapping,
...printOptions,
});
} else {
// add-to-queue mode
const data: PrintQueueItemCreate = {
// Reprint mode - start print immediately
if (isLibraryFile) {
await api.printLibraryFile(libraryFileId!, printerId, {
ams_mapping: amsMapping,
...printOptions,
});
} else {
await api.reprintArchive(archiveId!, printerId, {
plate_id: selectedPlate ?? undefined,
ams_mapping: amsMapping,
...printOptions,
});
}
} else if (mode === 'edit-queue-item' && i === 0) {
// Edit mode - update the original queue item for the first printer
const updateData: PrintQueueItemUpdate = {
printer_id: printerId,
archive_id: archiveId,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
manual_start: scheduleOptions.scheduleType === 'manual',
ams_mapping: amsMapping,
plate_id: selectedPlate,
scheduled_time: scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime
? new Date(scheduleOptions.scheduledTime).toISOString()
: null,
...printOptions,
};
if (scheduleOptions.scheduleType === 'scheduled' && scheduleOptions.scheduledTime) {
data.scheduled_time = new Date(scheduleOptions.scheduledTime).toISOString();
}
await addToQueueMutation.mutateAsync(data);
await updateQueueMutation.mutateAsync(updateData);
} else {
// Add-to-queue mode OR edit mode with additional printers
await addToQueueMutation.mutateAsync(getQueueData(printerId));
}
results.success++;
} catch (error) {
@@ -293,9 +327,9 @@ export function PrintModal({
// Show result toast
if (results.failed === 0) {
const action = mode === 'reprint' ? 'sent to' : 'queued for';
const action = mode === 'reprint' ? 'sent to' : (mode === 'edit-queue-item' ? 'updated/queued for' : 'queued for');
if (results.success === 1) {
showToast(`Print ${action} printer`);
showToast(mode === 'edit-queue-item' ? 'Queue item updated' : `Print ${action} printer`);
} else {
showToast(`Print ${action} ${results.success} printers`);
}
@@ -315,27 +349,22 @@ export function PrintModal({
const canSubmit = useMemo(() => {
if (isPending) return false;
// For edit mode, printer can be null (unassigned)
if (mode === 'edit-queue-item') {
return (printers?.length ?? 0) > 0;
}
// For reprint and add-to-queue, need at least one selected printer
// Need at least one selected printer
if (selectedPrinters.length === 0) return false;
// For multi-plate files, need a selected plate
if (isMultiPlate && !selectedPlate) return false;
// For multi-plate archive files, need a selected plate (library files skip this)
if (!isLibraryFile && isMultiPlate && !selectedPlate) return false;
return true;
}, [mode, selectedPrinters.length, isMultiPlate, selectedPlate, isPending, printers]);
}, [selectedPrinters.length, isMultiPlate, selectedPlate, isPending, isLibraryFile]);
// Modal title and action button text based on mode
const getModalConfig = () => {
const printerCount = isMultiPrinterMode ? selectedPrinters.length : 1;
const printerCount = selectedPrinters.length;
if (mode === 'reprint') {
return {
title: 'Re-print',
title: isLibraryFile ? 'Print' : 'Re-print',
icon: Printer,
submitText: printerCount > 1 ? `Print to ${printerCount} Printers` : 'Print',
submitIcon: Printer,
@@ -355,12 +384,15 @@ export function PrintModal({
: 'Adding...',
};
}
// edit-queue-item mode
return {
title: 'Edit Queue Item',
icon: Pencil,
submitText: 'Save Changes',
submitText: 'Save',
submitIcon: Pencil,
loadingText: 'Saving...',
loadingText: submitProgress.total > 1
? `Saving ${submitProgress.current}/${submitProgress.total}...`
: 'Saving...',
};
};
@@ -368,8 +400,13 @@ export function PrintModal({
const TitleIcon = modalConfig.icon;
const SubmitIcon = modalConfig.submitIcon;
// Show filament mapping only when single printer selected
const showFilamentMapping = effectivePrinterId && (isMultiPlate ? selectedPlate !== null : true);
// Show filament mapping when:
// - Single printer selected
// - For archives: plate is selected (for multi-plate) or not required (single-plate)
// - For library files: always show (no plate selection)
const showFilamentMapping = effectivePrinterId && (
isLibraryFile || (isMultiPlate ? selectedPlate !== null : true)
);
return (
<div
@@ -401,8 +438,7 @@ export function PrintModal({
<p className={`text-sm text-bambu-gray ${mode === 'reprint' ? 'mb-4' : ''}`}>
{mode === 'reprint' ? (
<>
Send <span className="text-white">{archiveName}</span> to{' '}
{isMultiPrinterMode ? 'printer(s)' : 'a printer'}
Send <span className="text-white">{archiveName}</span> to printer(s)
</>
) : (
<>
@@ -415,17 +451,15 @@ export function PrintModal({
{/* Printer selection */}
<PrinterSelector
printers={printers || []}
selectedPrinterId={selectedPrinter}
selectedPrinterIds={selectedPrinters}
onSelect={setSelectedPrinter}
onMultiSelect={setSelectedPrinters}
isLoading={loadingPrinters}
allowUnassigned={mode === 'edit-queue-item'}
allowMultiple={isMultiPrinterMode}
allowMultiple={true}
showInactive={mode === 'edit-queue-item'}
/>
{/* Multi-printer filament mapping note */}
{isMultiPrinterMode && selectedPrinters.length > 1 && (
{selectedPrinters.length > 1 && (
<div className="flex items-start gap-2 p-3 mb-2 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 text-blue-400 mt-0.5 flex-shrink-0" />
<p className="text-blue-400">
@@ -442,13 +476,21 @@ export function PrintModal({
onSelect={setSelectedPlate}
/>
{/* Filament mapping - show when single printer selected and plate ready */}
{showFilamentMapping && (
{/* Warning when archive data couldn't be loaded */}
{archiveDataMissing && (
<div className="flex items-start gap-2 p-3 mb-2 bg-orange-500/10 border border-orange-500/30 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 text-orange-400 mt-0.5 flex-shrink-0" />
<p className="text-orange-400">
Archive data unavailable. The source file may have been deleted. Filament mapping is disabled.
</p>
</div>
)}
{/* Filament mapping - show when printer selected and filament requirements available */}
{showFilamentMapping && !archiveDataMissing && (
<FilamentMapping
printerId={effectivePrinterId!}
archiveId={archiveId}
selectedPlate={selectedPlate}
isMultiPlate={isMultiPlate}
filamentReqs={effectiveFilamentReqs}
manualMappings={manualMappings}
onManualMappingChange={setManualMappings}
/>
+28 -11
View File
@@ -10,13 +10,19 @@ export type PrintModalMode = 'reprint' | 'add-to-queue' | 'edit-queue-item';
/**
* Props for the unified PrintModal component.
*
* Either archiveId or libraryFileId must be provided.
* - archiveId: For reprinting/queueing archives
* - libraryFileId: For printing library files directly
*/
export interface PrintModalProps {
/** Modal operation mode */
mode: PrintModalMode;
/** Archive ID to print */
archiveId: number;
/** Archive display name */
/** Archive ID to print (mutually exclusive with libraryFileId) */
archiveId?: number;
/** Library file ID to print (mutually exclusive with archiveId) */
libraryFileId?: number;
/** Display name for the print */
archiveName: string;
/** Existing queue item (only for edit-queue-item mode) */
queueItem?: PrintQueueItem;
@@ -103,13 +109,12 @@ export interface PlatesResponse {
*/
export interface PrinterSelectorProps {
printers: Printer[];
selectedPrinterId: number | null;
selectedPrinterIds?: number[];
onSelect: (printerId: number | null) => void;
onMultiSelect?: (printerIds: number[]) => void;
selectedPrinterIds: number[];
onMultiSelect: (printerIds: number[]) => void;
isLoading?: boolean;
allowUnassigned?: boolean;
allowMultiple?: boolean;
/** Show inactive printers (for edit mode where original assignment may be inactive) */
showInactive?: boolean;
}
/**
@@ -122,14 +127,26 @@ export interface PlateSelectorProps {
onSelect: (plateIndex: number) => void;
}
/**
* Filament requirement data structure.
*/
export interface FilamentReqsData {
filaments: Array<{
slot_id: number;
type: string;
color: string;
used_grams: number;
used_meters: number;
}>;
}
/**
* Props for the FilamentMapping component.
*/
export interface FilamentMappingProps {
printerId: number;
archiveId: number;
selectedPlate: number | null;
isMultiPlate: boolean;
/** Pre-fetched filament requirements data */
filamentReqs: FilamentReqsData | undefined;
manualMappings: Record<number, number>;
onManualMappingChange: (mappings: Record<number, number>) => void;
}