From e69d0b86105bc9d8acbbdaeb67cc354fbde0df68 Mon Sep 17 00:00:00 2001 From: AneoPsy Date: Wed, 4 Mar 2026 02:45:39 -1000 Subject: [PATCH] [Fix]: AMS slot modal infinite scroll loop on Windows (#580) * Fix AMS slot modal not scrolling to preselected filament The scroll useEffect fired before the loading spinner was replaced by the preset list, so the DOM element didn't exist yet. The ref guard was then set, preventing any retry once buttons appeared. - Move isLoading computation before the scroll effect and add it as a dependency so the effect re-runs when loading completes - Gate scroll on !isLoading so it only runs when buttons exist - Only mark scrolledToRef after the element is actually found - Use requestAnimationFrame to scroll after browser layout - Use block:'center' so the selected item is clearly visible Preselect filament for printer-configured AMS slots When an AMS slot was configured directly on the printer (no savedPresetId), the modal didn't preselect the filament even though it was in the list. Add a third fallback that matches trayInfoIdx against the full filteredPresets list (cloud and builtin sources). * Restore scrolledToRef lost during merge The useRef import, scrolledToRef declaration, and its reset were dropped during the 0.2.2b1 merge, causing a ReferenceError in the scroll-to-preselected-filament effect. * Fix preset scroll loop and wire up useEffect scroll logic - Remove inline ref callbacks with scrollIntoView on both preset button lists (fullscreen + standard) to stop the infinite scroll loop on Windows - Add data-preset-id attribute so the existing useEffect/querySelector scroll logic can actually find the elements - Replace filteredPresets dependency with builtinFilaments in the selection useEffect to prevent search keystrokes from overriding manual preset selection * Adjust scroll behavior in ConfigureAmsSlotModal --------- Co-authored-by: MartinNYHC --- .../src/components/ConfigureAmsSlotModal.tsx | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ConfigureAmsSlotModal.tsx b/frontend/src/components/ConfigureAmsSlotModal.tsx index ef83191fd..dad4ddb23 100644 --- a/frontend/src/components/ConfigureAmsSlotModal.tsx +++ b/frontend/src/components/ConfigureAmsSlotModal.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useCallback } from 'react'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react'; @@ -256,6 +256,7 @@ export function ConfigureAmsSlotModal({ const [searchQuery, setSearchQuery] = useState(''); const [showSuccess, setShowSuccess] = useState(false); const [showExtendedColors, setShowExtendedColors] = useState(false); + const scrolledToRef = useRef(''); // Fetch cloud settings (gracefully handle 401 when logged out) const { data: cloudSettings, isLoading: settingsLoading, isError: cloudError } = useQuery({ @@ -739,6 +740,13 @@ export function ConfigureAmsSlotModal({ if (currentPreset) { setSelectedPresetId(currentPreset.setting_id); } + } else if (slotInfo.trayInfoIdx && builtinFilaments?.length) { + // Last resort: match trayInfoIdx against builtin presets + const trayIdx = slotInfo.trayInfoIdx; + const match = builtinFilaments.find(bf => bf.filament_id === trayIdx); + if (match) { + setSelectedPresetId(`builtin_${match.filament_id}`); + } } // Pre-populate color from current slot (black is valid — empty slots don't pass trayColor) @@ -756,8 +764,9 @@ export function ConfigureAmsSlotModal({ setColorInput(''); setSearchQuery(''); setShowSuccess(false); + scrolledToRef.current = ''; } - }, [isOpen, slotInfo.savedPresetId, slotInfo.trayInfoIdx, slotInfo.trayColor, cloudSettings?.filament]); + }, [isOpen, slotInfo.savedPresetId, slotInfo.trayInfoIdx, slotInfo.trayColor, cloudSettings?.filament, builtinFilaments]); // Auto-select best matching K profile when preset changes useEffect(() => { @@ -791,9 +800,26 @@ export function ConfigureAmsSlotModal({ } }, [isOpen, handleKeyDown]); - if (!isOpen) return null; - const isLoading = (settingsLoading && !cloudError) || localLoading || builtinLoading || kprofilesLoading; + + // Scroll selected preset into view when data finishes loading or the selection changes. + // Uses a ref guard so scrollIntoView only fires once per selection, preventing the + // infinite scroll loop that occurred on Windows with inline callback refs. + useEffect(() => { + if (!isLoading && selectedPresetId && selectedPresetId !== scrolledToRef.current) { + const raf = requestAnimationFrame(() => { + const modal = document.querySelector('[class*="fixed inset-0 z-50"]'); + const el = modal?.querySelector(`[data-preset-id="${CSS.escape(selectedPresetId)}"]`); + if (el) { + scrolledToRef.current = selectedPresetId; + el.scrollIntoView({ block: 'nearest' }); + } + }); + return () => cancelAnimationFrame(raf); + } + }, [selectedPresetId, isLoading]); + + if (!isOpen) return null; const canSave = selectedPresetId && !configureMutation.isPending; // Get display color (custom or slot default) @@ -910,9 +936,7 @@ export function ConfigureAmsSlotModal({ filteredPresets.map((preset) => (