Improve spool ID search and AMS assignment (issue #2978) (#2998)

This commit is contained in:
Peer Drescher
2026-09-28 16:42:38 +02:00
committed by GitHub
parent a06bb05624
commit 632930f02a
27 changed files with 781 additions and 190 deletions
+46 -13
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { X, Loader2, Save, Beaker, Palette, Zap, Tag, Unlink } from 'lucide-react';
import { X, Loader2, Save, Beaker, Palette, Zap, Tag, Link2, Unlink } from 'lucide-react';
import { api, ApiError } from '../api/client';
import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset, BuiltinFilament, SpoolmanBulkCreateResult, SpoolFilamentPresetInput, SpoolKProfileInput, SpoolmanFilamentEntry } from '../api/client';
import { Button } from './Button';
@@ -25,6 +25,7 @@ import { SpoolmanFilamentPicker } from './spool-form/SpoolmanFilamentPicker';
import { PrinterProfilesSection } from './spool-form/PrinterProfilesSection';
import { normaliseFlow } from '../utils/nozzleFlow';
import { SpoolUsageHistory } from './SpoolUsageHistory';
import { AssignToAmsModal } from './spoolbuddy/AssignToAmsModal';
import {
invalidateInventoryLocations,
invalidateSpoolAndLocationQueries,
@@ -91,6 +92,11 @@ export function SpoolFormModal({
const [supplierLinksTouched, setSupplierLinksTouched] = useState(false);
const [quickAdd, setQuickAdd] = useState(false);
const [quantity, setQuantity] = useState(1);
const [showAssignModal, setShowAssignModal] = useState(false);
useEffect(() => {
setShowAssignModal(false);
}, [isOpen, spool?.id]);
// Cloud presets
const [cloudAuthenticated, setCloudAuthenticated] = useState(false);
@@ -729,12 +735,12 @@ export function SpoolFormModal({
// the slot assignment lives in the spoolman_slot_assignments table keyed by
// spoolman_spool_id, not in the legacy spool_assignments table — #1336 was the
// resulting "Unassign button is always disabled" report.
const { data: assignments } = useQuery({
const { data: assignments, isLoading: assignmentsLoading } = useQuery({
queryKey: ['spool-assignments'],
queryFn: () => api.getAssignments(),
enabled: isOpen && isEditing && !spoolmanMode,
});
const { data: spoolmanSlotAssignments } = useQuery({
const { data: spoolmanSlotAssignments, isLoading: spoolmanAssignmentsLoading } = useQuery({
queryKey: ['spoolman-slot-assignments-all'],
queryFn: () => api.getSpoolmanSlotAssignments(),
enabled: isOpen && isEditing && spoolmanMode,
@@ -746,6 +752,7 @@ export function SpoolFormModal({
}
return assignments?.find(a => a.spool_id === spool.id);
})();
const assignmentLoading = spoolmanMode ? spoolmanAssignmentsLoading : assignmentsLoading;
// Read inventory + settings caches (already populated by InventoryPage) to
// drive the category autocomplete and low-stock-threshold placeholder. #729
@@ -910,11 +917,14 @@ export function SpoolFormModal({
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
// The nested AMS assignment dialog owns Escape while it is open. Both
// listeners live on document, so propagation control in the child would
// not prevent this editor handler from running for the same key press.
if (e.key === 'Escape' && !showAssignModal) onClose();
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
}, [isOpen, onClose, showAssignModal]);
if (!isOpen) return null;
@@ -985,6 +995,7 @@ export function SpoolFormModal({
const isPending = createMutation.isPending || bulkCreateMutation.isPending || updateMutation.isPending || deleteTagMutation.isPending || unassignMutation.isPending;
return (
<>
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
@@ -1242,14 +1253,25 @@ export function SpoolFormModal({
<Tag className="w-4 h-4" />
{t('inventory.clearRfid', 'Clear RFID Tag')}
</Button>
<Button
variant="secondary"
onClick={() => unassignMutation.mutate()}
disabled={isPending || !spoolAssignment}
>
<Unlink className="w-4 h-4" />
{t('inventory.unassignSpool', 'Unassign')}
</Button>
{spoolAssignment ? (
<Button
variant="secondary"
onClick={() => unassignMutation.mutate()}
disabled={isPending || assignmentLoading}
>
<Unlink className="w-4 h-4" />
{t('inventory.unassignSpool', 'Unassign')}
</Button>
) : (
<Button
variant="secondary"
onClick={() => setShowAssignModal(true)}
disabled={isPending || assignmentLoading}
>
<Link2 className="w-4 h-4" />
{t('inventory.assignSpool', 'Assign Spool')}
</Button>
)}
</div>
)}
<div className="flex gap-2 ml-auto">
@@ -1276,5 +1298,16 @@ export function SpoolFormModal({
</div>
</div>
</div>
{isEditing && spool && (
<AssignToAmsModal
isOpen={showAssignModal}
onClose={() => setShowAssignModal(false)}
spool={spool}
printerId={null}
variant="dialog"
spoolmanMode={spoolmanMode}
/>
)}
</>
);
}