mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-10 07:55:39 +02:00
Add material/profile mismatch warnings to SpoolBuddy Assign-to-AMS modal
The AssignToAmsModal was missing all material/profile validation guards that exist in the main AssignSpoolModal. Clicking an AMS slot now checks the spool's material and slicer profile against the target slot's current filament, showing a confirmation dialog on mismatch. Respects the global disable_filament_warnings setting. Uses existing i18n keys and ConfirmModal.
This commit is contained in:
@@ -17,6 +17,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Spool Notes in Assign Spool Modal** ([#793](https://github.com/maziggy/bambuddy/issues/793)) — Spool cards in the Assign Spool modal now show the spool's note as a hover tooltip, making it easier to identify spools by tracking IDs or other metadata stored in notes. Works with both internal inventory and Spoolman-synced spools. Requested by @LegionCanadian.
|
||||
- **WiFi Safeguard for SpoolBuddy Pi** — The install script now drops an APT hook (`/etc/apt/apt.conf.d/80-preserve-wifi`) that backs up NetworkManager WiFi connections before every `apt upgrade` and restores them if they get wiped. Prevents headless SpoolBuddy Pis from losing WiFi connectivity after Raspberry Pi OS package upgrades (observed with Bookworm kernel/raspi-config updates that clear `/etc/NetworkManager/system-connections/`).
|
||||
- **SpoolBuddy Install Script Now Upgrades System Packages** — The install script now runs `apt-get upgrade -y` after installing required packages and the WiFi safeguard. This ensures the Pi is fully up to date before SpoolBuddy is deployed, and the WiFi safeguard protects connectivity during the upgrade.
|
||||
- **SpoolBuddy Assign-to-AMS Material Mismatch Warnings** — The SpoolBuddy "Assign to AMS" modal now warns when the spool's material or slicer profile doesn't match the target slot's current filament. Shows a confirmation dialog with five warning levels: exact material mismatch, partial material match, profile-only mismatch, and combined material+profile mismatches. Respects the global `disable_filament_warnings` setting. Previously, assigning a spool to an occupied slot proceeded without any validation, matching the behavior already present in the main Assign Spool modal.
|
||||
|
||||
### Fixed
|
||||
- **Delete Tag Leaves Stale Tag Type** — The "Delete Tag" button in the spool edit modal only cleared `tag_uid` but left `tray_uuid`, `tag_type`, and `data_origin` intact. All tag-related fields are now cleared together.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { X, Loader2, CheckCircle, XCircle, Layers } from 'lucide-react';
|
||||
import { api, type InventorySpool, type PrinterStatus, type AMSTray } from '../../api/client';
|
||||
import { ConfirmModal } from '../ConfirmModal';
|
||||
import { AmsUnitCard, NozzleBadge } from './AmsUnitCard';
|
||||
import type { AmsThresholds } from './AmsUnitCard';
|
||||
import { getFillBarColor } from '../../utils/amsHelpers';
|
||||
@@ -22,6 +23,34 @@ function trayColorToCSS(color: string | null): string {
|
||||
return `#${color.slice(0, 6)}`;
|
||||
}
|
||||
|
||||
// --- Material/profile mismatch helpers (pure functions, no component state) ---
|
||||
const normalizeValue = (value: string | undefined | null) =>
|
||||
(value ?? '').trim().toUpperCase();
|
||||
|
||||
function checkMaterialMatch(
|
||||
spoolMaterial: string | undefined | null,
|
||||
trayMaterial: string | undefined | null
|
||||
): 'exact' | 'partial' | 'none' {
|
||||
const normalizedSpool = normalizeValue(spoolMaterial);
|
||||
const normalizedTray = normalizeValue(trayMaterial);
|
||||
if (!normalizedSpool || !normalizedTray) return 'none';
|
||||
if (normalizedSpool === normalizedTray) return 'exact';
|
||||
if (normalizedTray.includes(normalizedSpool) || normalizedSpool.includes(normalizedTray)) {
|
||||
return 'partial';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
function checkProfileMatch(
|
||||
spoolProfile: string | undefined | null,
|
||||
trayProfile: string | undefined | null
|
||||
): boolean {
|
||||
const normalizedSpoolProfile = normalizeValue(spoolProfile);
|
||||
const normalizedTrayProfile = normalizeValue(trayProfile);
|
||||
if (!normalizedSpoolProfile || !normalizedTrayProfile) return false;
|
||||
return normalizedSpoolProfile === normalizedTrayProfile;
|
||||
}
|
||||
|
||||
interface AssignToAmsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -34,11 +63,24 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId }: AssignTo
|
||||
const queryClient = useQueryClient();
|
||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
||||
const [statusType, setStatusType] = useState<'info' | 'success' | 'error' | null>(null);
|
||||
const [showMismatchConfirm, setShowMismatchConfirm] = useState(false);
|
||||
const [mismatchDetails, setMismatchDetails] = useState<{
|
||||
type: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile';
|
||||
spoolMaterial: string;
|
||||
trayMaterial: string;
|
||||
spoolProfile?: string;
|
||||
trayProfile?: string;
|
||||
location: string;
|
||||
} | null>(null);
|
||||
const [pendingSlot, setPendingSlot] = useState<{ amsId: number; trayId: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setStatusMessage(null);
|
||||
setStatusType(null);
|
||||
setShowMismatchConfirm(false);
|
||||
setMismatchDetails(null);
|
||||
setPendingSlot(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -155,12 +197,78 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId }: AssignTo
|
||||
|
||||
const isWaiting = configureMutation.isPending;
|
||||
|
||||
const handleSlotClick = useCallback((amsId: number, trayId: number) => {
|
||||
if (isWaiting) return;
|
||||
const getTrayForSlot = useCallback((amsId: number, trayId: number): AMSTray | null => {
|
||||
if (amsId === 254 || amsId === 255) {
|
||||
const extTrayId = amsId === 254 ? 254 : 254 + trayId;
|
||||
return vtTrays.find(t => (t.id ?? 254) === extTrayId) || null;
|
||||
}
|
||||
const unit = amsUnits.find(u => u.id === amsId);
|
||||
return unit?.tray?.find(t => t.id === trayId) || null;
|
||||
}, [amsUnits, vtTrays]);
|
||||
|
||||
const getSlotLocationLabel = useCallback((amsId: number, trayId: number): string => {
|
||||
if (amsId <= 3) return `${getAmsName(amsId)} ${t('ams.slot', 'Slot')} ${trayId + 1}`;
|
||||
if (amsId >= 128 && amsId <= 135) return getAmsName(amsId);
|
||||
if (amsId === 254) return t('printers.extL', 'Ext-L');
|
||||
return isDualNozzle ? t('printers.extR', 'Ext-R') : t('printers.ext', 'Ext');
|
||||
}, [t, isDualNozzle]);
|
||||
|
||||
const doAssign = useCallback((amsId: number, trayId: number) => {
|
||||
setStatusType('info');
|
||||
setStatusMessage(t('spoolbuddy.modal.assigning', 'Configuring slot...'));
|
||||
configureMutation.mutate({ amsId, trayId });
|
||||
}, [isWaiting, configureMutation, t]);
|
||||
}, [configureMutation, t]);
|
||||
|
||||
const handleSlotClick = useCallback((amsId: number, trayId: number) => {
|
||||
if (isWaiting) return;
|
||||
|
||||
if (!settings?.disable_filament_warnings) {
|
||||
const tray = getTrayForSlot(amsId, trayId);
|
||||
if (tray && !isTrayEmpty(tray)) {
|
||||
const trayMaterial = tray.tray_sub_brands || tray.tray_type || '';
|
||||
const materialMatchResult = checkMaterialMatch(spool.material, trayMaterial);
|
||||
const spoolProfile = spool.slicer_filament_name || spool.slicer_filament;
|
||||
const trayProfile = tray.tray_type || '';
|
||||
const profileMatches = checkProfileMatch(spoolProfile, trayProfile);
|
||||
|
||||
if (materialMatchResult !== 'exact' || !profileMatches) {
|
||||
let mismatchType: 'material' | 'partial' | 'profile' | 'material_profile' | 'partial_profile' = 'profile';
|
||||
if (materialMatchResult === 'none' && !profileMatches) {
|
||||
mismatchType = 'material_profile';
|
||||
} else if (materialMatchResult === 'partial' && !profileMatches) {
|
||||
mismatchType = 'partial_profile';
|
||||
} else if (materialMatchResult === 'none') {
|
||||
mismatchType = 'material';
|
||||
} else if (materialMatchResult === 'partial') {
|
||||
mismatchType = 'partial';
|
||||
}
|
||||
|
||||
const location = getSlotLocationLabel(amsId, trayId);
|
||||
setPendingSlot({ amsId, trayId });
|
||||
setMismatchDetails({
|
||||
type: mismatchType,
|
||||
spoolMaterial: spool.material || '',
|
||||
trayMaterial: trayMaterial || '',
|
||||
spoolProfile: spoolProfile || undefined,
|
||||
trayProfile: trayProfile || undefined,
|
||||
location,
|
||||
});
|
||||
setShowMismatchConfirm(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doAssign(amsId, trayId);
|
||||
}, [isWaiting, settings?.disable_filament_warnings, spool, getTrayForSlot, getSlotLocationLabel, doAssign]);
|
||||
|
||||
const handleConfirmMismatch = useCallback(() => {
|
||||
if (!pendingSlot) return;
|
||||
setShowMismatchConfirm(false);
|
||||
setMismatchDetails(null);
|
||||
doAssign(pendingSlot.amsId, pendingSlot.trayId);
|
||||
setPendingSlot(null);
|
||||
}, [pendingSlot, doAssign]);
|
||||
|
||||
// Build single-slot items (HT + External)
|
||||
const singleSlots = useMemo(() => {
|
||||
@@ -213,6 +321,7 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId }: AssignTo
|
||||
const colorHex = spool.rgba ? `#${spool.rgba.slice(0, 6)}` : '#808080';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-[60] bg-bambu-dark flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0">
|
||||
@@ -358,5 +467,68 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId }: AssignTo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMismatchConfirm && mismatchDetails && (() => {
|
||||
let message = '';
|
||||
|
||||
if (mismatchDetails.type === 'material') {
|
||||
message = t('inventory.assignMismatchMessage', {
|
||||
spoolMaterial: mismatchDetails.spoolMaterial,
|
||||
trayMaterial: mismatchDetails.trayMaterial,
|
||||
location: mismatchDetails.location,
|
||||
});
|
||||
} else if (mismatchDetails.type === 'partial') {
|
||||
message = t('inventory.assignPartialMismatchMessage', {
|
||||
spoolMaterial: mismatchDetails.spoolMaterial,
|
||||
trayMaterial: mismatchDetails.trayMaterial,
|
||||
location: mismatchDetails.location,
|
||||
});
|
||||
} else if (mismatchDetails.type === 'material_profile') {
|
||||
message = `${t('inventory.assignMismatchMessage', {
|
||||
spoolMaterial: mismatchDetails.spoolMaterial,
|
||||
trayMaterial: mismatchDetails.trayMaterial,
|
||||
location: mismatchDetails.location,
|
||||
})}\n\n${t('inventory.assignProfileMismatchMessage', {
|
||||
spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'),
|
||||
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
|
||||
location: mismatchDetails.location,
|
||||
})}`;
|
||||
} else if (mismatchDetails.type === 'partial_profile') {
|
||||
message = `${t('inventory.assignPartialMismatchMessage', {
|
||||
spoolMaterial: mismatchDetails.spoolMaterial,
|
||||
trayMaterial: mismatchDetails.trayMaterial,
|
||||
location: mismatchDetails.location,
|
||||
})}\n\n${t('inventory.assignProfileMismatchMessage', {
|
||||
spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'),
|
||||
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
|
||||
location: mismatchDetails.location,
|
||||
})}`;
|
||||
} else if (mismatchDetails.type === 'profile') {
|
||||
message = t('inventory.assignProfileMismatchMessage', {
|
||||
spoolProfile: mismatchDetails.spoolProfile || t('common.unknown'),
|
||||
trayProfile: mismatchDetails.trayProfile || t('common.unknown'),
|
||||
location: mismatchDetails.location,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
title={t('inventory.assignMismatchTitle')}
|
||||
message={message}
|
||||
confirmText={t('inventory.assignMismatchConfirm')}
|
||||
variant="warning"
|
||||
isLoading={configureMutation.isPending}
|
||||
onConfirm={handleConfirmMismatch}
|
||||
onCancel={() => {
|
||||
if (!configureMutation.isPending) {
|
||||
setShowMismatchConfirm(false);
|
||||
setPendingSlot(null);
|
||||
setMismatchDetails(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-Ed-P8IU2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CS1ycNG7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CZLTApPU.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user