mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
● fix(ams): restore Configure/Assign actions on reset slots + relax Assign Spool filtering (#1047)
Three related fixes reported together:
(1) After resetting an AMS slot, the printer card showed "Empty Slot"
with no Configure or Assign Spool actions while SpoolBuddy's AMS page
still let the user re-configure the same slot. Commit c9efa4b8 (#784)
added a `tray?.state === 10` gate to the EmptySlotHoverCard actions,
intended to hide them on physically-empty slots (state=9). In practice
firmware often reports state=9 (or omits state entirely) after a
user-initiated reset even when a spool is still present, so the gate
hit the wrong case. The gate was redundant anyway — EmptySlotHoverCard
only renders when tray_type is empty — so it's removed at both the
standard-AMS and AMS-HT render paths.
(2) After configuring a slot with a Generic profile, the Assign Spool
modal hid manually-added inventory spools even when material matched,
unless the user flipped "Show all spools". The filter required exact
slicer_filament_name equality, which manually-added spools don't
populate. Filter now prefers exact slicer-profile match when both
sides have one, and falls back to partial material match in either
direction (so a "PLA" spool shows up for a "PLA Basic" slot).
(3) On assign, the mismatch dialog fired on every Generic spool
because Bambu Studio / OrcaSlicer profile names carry an @printer
nozzle (variant) qualifier while the tray stores the bare base name.
Both the filter and checkProfileMatch now strip everything from @
onward before comparing.
Adds 3 regression tests covering each path.
This commit is contained in:
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [0.2.4b1] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- **AMS: Configure / Assign Spool Hidden on Reset Slots, and Assign Spool Missing Matching-Material Inventory** ([#1047](https://github.com/maziggy/bambuddy/issues/1047)) — Two separate symptoms from the same report. (1) After resetting an AMS slot from the printer UI, the Bambuddy printer card showed "Empty Slot" with no Configure or Assign Spool actions on hover, while the same slot in SpoolBuddy's AMS page still let the user re-configure it. Root cause: commit `c9efa4b8` (#784) added a `tray?.state === 10` gate to the `EmptySlotHoverCard` actions, intended to show the buttons only when a spool was physically present but not loaded (state=10) and hide them on truly empty slots (state=9). In practice, firmware often reports `state=9` (or no `state` field at all) after a user-initiated reset — even when a spool is still physically in the slot — so the actions disappeared exactly when the user needed them. The gate is redundant anyway (`EmptySlotHoverCard` is only rendered when the slot has no `tray_type`, so it's definitionally empty from Bambuddy's perspective), and configuring an empty slot is a valid "tell the printer what will be loaded here" operation. The gate is now removed at both the standard-AMS and AMS-HT render paths. (2) After configuring a slot with a Generic profile (e.g. "Devil Design PLA Basic Red"), the Assign Spool modal didn't list the matching inventory spool unless the user enabled the "Show all spools" toggle. Root cause: the filter at `AssignSpoolModal.tsx:144` required `normalizeValue(spool.slicer_filament_name) === normalizeValue(trayInfo.profile)` — manually-added inventory spools typically don't have `slicer_filament_name` populated, so they failed the exact-profile check even when the material matched. The filter now prefers an exact slicer-profile match when both sides advertise one, and falls back to partial material match in either direction (so e.g. a spool with `material="PLA"` is selectable for a slot reporting `"PLA Basic"`) when profile info is missing. (3) Once the matching spool was assignable, a "profile mismatch" confirmation dialog still warned on every assignment because Bambu Studio / OrcaSlicer slicer-profile names carry a printer/nozzle/variant qualifier after `@` (e.g. `"Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)"`) while the tray stores only the bare base name (`"Devil Design PLA Basic"`), and `checkProfileMatch` compared the full strings. Both the filter and the mismatch check now strip the `@…` qualifier before comparing, so identical base profiles are treated as a match. Regression test covers a spool with no slicer profile being surfaced for a slot whose profile + material are both set. Thanks to @TravisWilder for the report.
|
||||
- **Skip Objects: Enlarged Preview Image Fails to Load on Auth-Enabled Instances** ([#1046](https://github.com/maziggy/bambuddy/issues/1046)) — Clicking the mini print-pr
|
||||
|
||||
|
||||
|
||||
@@ -134,4 +134,109 @@ describe('AssignSpoolModal', () => {
|
||||
expect(screen.getByText(/No manually added spools/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('lists spool with no slicer profile when material matches the tray (#1047)', async () => {
|
||||
const spoolWithoutSlicerProfile = {
|
||||
id: 10,
|
||||
material: 'PLA',
|
||||
subtype: 'Basic',
|
||||
brand: 'Devil Design',
|
||||
color_name: 'Red',
|
||||
rgba: 'FF0000FF',
|
||||
label_weight: 1000,
|
||||
weight_used: 0,
|
||||
tag_uid: null,
|
||||
tray_uuid: null,
|
||||
slicer_filament_name: null,
|
||||
slicer_filament: null,
|
||||
};
|
||||
(api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([spoolWithoutSlicerProfile]);
|
||||
|
||||
render(
|
||||
<AssignSpoolModal
|
||||
{...defaultProps}
|
||||
trayInfo={{
|
||||
type: 'PLA',
|
||||
material: 'PLA',
|
||||
profile: 'Devil Design PLA Basic',
|
||||
color: 'FF0000',
|
||||
location: 'AMS 1 - Slot 1',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('lists spool with shorter material when tray advertises a qualified variant (#1047)', async () => {
|
||||
// Spool.material = "PLA", tray material = "PLA Basic" — partial match in either direction.
|
||||
const shortMaterialSpool = {
|
||||
id: 11,
|
||||
material: 'PLA',
|
||||
subtype: 'Basic',
|
||||
brand: 'Devil Design',
|
||||
color_name: 'Red',
|
||||
rgba: 'FF0000FF',
|
||||
label_weight: 1000,
|
||||
weight_used: 0,
|
||||
tag_uid: null,
|
||||
tray_uuid: null,
|
||||
slicer_filament_name: null,
|
||||
slicer_filament: null,
|
||||
};
|
||||
(api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([shortMaterialSpool]);
|
||||
|
||||
render(
|
||||
<AssignSpoolModal
|
||||
{...defaultProps}
|
||||
trayInfo={{
|
||||
type: 'PLA Basic',
|
||||
material: 'PLA Basic',
|
||||
color: 'FF0000',
|
||||
location: 'AMS 1 - Slot 1',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('lists spool whose slicer profile has an @printer qualifier that strips to the tray profile (#1047)', async () => {
|
||||
const qualifiedProfileSpool = {
|
||||
id: 12,
|
||||
material: 'PLA',
|
||||
subtype: 'Basic',
|
||||
brand: 'Devil Design',
|
||||
color_name: 'Red',
|
||||
rgba: 'FF0000FF',
|
||||
label_weight: 1000,
|
||||
weight_used: 0,
|
||||
tag_uid: null,
|
||||
tray_uuid: null,
|
||||
slicer_filament_name: 'Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)',
|
||||
};
|
||||
// Use a non-matching material to force the filter to rely on the profile path only.
|
||||
(api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([qualifiedProfileSpool]);
|
||||
|
||||
render(
|
||||
<AssignSpoolModal
|
||||
{...defaultProps}
|
||||
trayInfo={{
|
||||
type: 'ABS',
|
||||
material: 'ABS',
|
||||
profile: 'Devil Design PLA Basic',
|
||||
color: 'FF0000',
|
||||
location: 'AMS 1 - Slot 1',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,12 +111,18 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
|
||||
return 'none';
|
||||
};
|
||||
|
||||
// Bambu Studio / OrcaSlicer profile names carry a printer/nozzle/variant qualifier after
|
||||
// `@` (e.g. "Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)"), while the tray's
|
||||
// profile is typically the bare base name. Strip the qualifier before comparing so identical
|
||||
// base profiles don't trigger a mismatch warning (#1047).
|
||||
const stripProfileQualifier = (value: string) => value.split('@')[0].trim();
|
||||
|
||||
const checkProfileMatch = (
|
||||
spoolProfile: string | undefined | null,
|
||||
trayProfile: string | undefined | null
|
||||
): boolean => {
|
||||
const normalizedSpoolProfile = normalizeValue(spoolProfile);
|
||||
const normalizedTrayProfile = normalizeValue(trayProfile);
|
||||
const normalizedSpoolProfile = stripProfileQualifier(normalizeValue(spoolProfile));
|
||||
const normalizedTrayProfile = stripProfileQualifier(normalizeValue(trayProfile));
|
||||
|
||||
if (!normalizedSpoolProfile || !normalizedTrayProfile) return false;
|
||||
|
||||
@@ -138,14 +144,29 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
|
||||
!assignedSpoolIds.has(spool.id) && (isExternalSlot || (!spool.tag_uid && !spool.tray_uuid))
|
||||
);
|
||||
|
||||
// Filtering logic with toggle: search filter always applies, AMS tray profile filter is optional
|
||||
// Filtering logic with toggle: search filter always applies, AMS tray profile filter is optional.
|
||||
// Show a spool if EITHER the slicer profile matches exactly OR the material overlaps with the
|
||||
// tray's material (partial-match both directions — "PLA" spool accepts a "PLA Basic" slot and
|
||||
// vice versa). Manually-added inventory spools typically have no slicer_filament_name; gating
|
||||
// on strict profile equality alone hid them even when the material matched (#1047).
|
||||
let filteredSpools = manualSpools;
|
||||
if (!disableFiltering) {
|
||||
if (trayInfo?.profile || trayInfo?.type) {
|
||||
const trayProfile = normalizeValue(trayInfo.profile || trayInfo.type);
|
||||
const trayProfile = stripProfileQualifier(normalizeValue(trayInfo?.profile));
|
||||
const trayMaterial = normalizeValue(trayInfo?.material || trayInfo?.type);
|
||||
if (trayProfile || trayMaterial) {
|
||||
filteredSpools = filteredSpools?.filter((spool: InventorySpool) => {
|
||||
const spoolProfile = normalizeValue(spool.slicer_filament_name || spool.slicer_filament);
|
||||
return trayProfile && spoolProfile && spoolProfile === trayProfile;
|
||||
const spoolProfile = stripProfileQualifier(normalizeValue(spool.slicer_filament_name || spool.slicer_filament));
|
||||
const spoolMaterial = normalizeValue(spool.material);
|
||||
if (trayProfile && spoolProfile && spoolProfile === trayProfile) return true;
|
||||
if (trayMaterial && spoolMaterial) {
|
||||
return (
|
||||
spoolMaterial === trayMaterial ||
|
||||
trayMaterial.includes(spoolMaterial) ||
|
||||
spoolMaterial.includes(trayMaterial)
|
||||
);
|
||||
}
|
||||
// Neither side has filterable info on whatever dimension remains — show it.
|
||||
return !spoolProfile && !spoolMaterial;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3552,7 +3552,7 @@ function PrinterCard({
|
||||
</FilamentHoverCard>
|
||||
) : (
|
||||
<EmptySlotHoverCard
|
||||
configureSlot={tray?.state === 10 ? {
|
||||
configureSlot={{
|
||||
enabled: hasPermission('printers:control'),
|
||||
onConfigure: () => setConfigureSlotModal({
|
||||
amsId: ams.id,
|
||||
@@ -3560,8 +3560,8 @@ function PrinterCard({
|
||||
trayCount: ams.tray.length,
|
||||
extruderId: mappedExtruderId,
|
||||
}),
|
||||
} : undefined}
|
||||
inventory={tray?.state === 10 && !spoolmanEnabled ? {
|
||||
}}
|
||||
inventory={spoolmanEnabled ? undefined : {
|
||||
onAssignSpool: () => setAssignSpoolModal({
|
||||
printerId: printer.id,
|
||||
amsId: ams.id,
|
||||
@@ -3572,7 +3572,7 @@ function PrinterCard({
|
||||
location: `${getAmsLabel(ams.id, ams.tray.length)} Slot ${slotIdx + 1}`,
|
||||
},
|
||||
}),
|
||||
} : undefined}
|
||||
}}
|
||||
>
|
||||
{slotVisual}
|
||||
</EmptySlotHoverCard>
|
||||
@@ -3870,7 +3870,7 @@ function PrinterCard({
|
||||
</FilamentHoverCard>
|
||||
) : (
|
||||
<EmptySlotHoverCard
|
||||
configureSlot={tray?.state === 10 ? {
|
||||
configureSlot={{
|
||||
enabled: hasPermission('printers:control'),
|
||||
onConfigure: () => setConfigureSlotModal({
|
||||
amsId: ams.id,
|
||||
@@ -3878,8 +3878,8 @@ function PrinterCard({
|
||||
trayCount: ams.tray.length,
|
||||
extruderId: mappedExtruderId,
|
||||
}),
|
||||
} : undefined}
|
||||
inventory={tray?.state === 10 && !spoolmanEnabled ? {
|
||||
}}
|
||||
inventory={spoolmanEnabled ? undefined : {
|
||||
onAssignSpool: () => setAssignSpoolModal({
|
||||
printerId: printer.id,
|
||||
amsId: ams.id,
|
||||
@@ -3890,7 +3890,7 @@ function PrinterCard({
|
||||
location: getAmsLabel(ams.id, ams.tray.length),
|
||||
},
|
||||
}),
|
||||
} : undefined}
|
||||
}}
|
||||
>
|
||||
{slotVisual}
|
||||
</EmptySlotHoverCard>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BVXFAk6e.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C60hlRK5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CkAOuJaW.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user