Browse Source

[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 <mz@v8w.de>
AneoPsy 2 months ago
parent
commit
e69d0b8610
1 changed files with 32 additions and 10 deletions
  1. 32 10
      frontend/src/components/ConfigureAmsSlotModal.tsx

+ 32 - 10
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<string>('');
 
   // 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) => (
                       <button
                         key={preset.id}
-                        ref={selectedPresetId === preset.id ? (el) => {
-                          el?.scrollIntoView({ block: 'nearest' });
-                        } : undefined}
+                        data-preset-id={preset.id}
                         onClick={() => setSelectedPresetId(preset.id)}
                         className={`w-full p-2 rounded-lg border text-left transition-colors ${
                           selectedPresetId === preset.id
@@ -1147,9 +1171,7 @@ export function ConfigureAmsSlotModal({
                       filteredPresets.map((preset) => (
                         <button
                           key={preset.id}
-                          ref={selectedPresetId === preset.id ? (el) => {
-                            el?.scrollIntoView({ block: 'nearest' });
-                          } : undefined}
+                          data-preset-id={preset.id}
                           onClick={() => setSelectedPresetId(preset.id)}
                           className={`w-full p-2 rounded-lg border text-left transition-colors ${
                             selectedPresetId === preset.id