Browse Source

fix(inventory): show all per-printer/per-nozzle variants in spool form's Slicer Preset dropdown (issue #1248)

  Two defects in buildFilamentOptions, surfaced together:

  1. The function was precedence-based — cloud presets short-circuited
     the local-presets branch, silently hiding any imported Local Profile
     while the user was logged into Bambu Cloud. The wiki documents the
     dropdown as "merged and deduplicated" across cloud + local + built-in.

  2. Cloud default presets and local presets were being collapsed by base
     name (everything after "@" stripped), so all P1S/X1C/A1 variants of
     "Bambu PLA Basic" rendered as a single row. The spool form is
     printer-agnostic by design, so the right semantic is to show every
     variant individually — the union across all printers — not collapse
     them. AMS Slot is per-printer (it filters), the spool form is
     union-of-all (it doesn't).

  Rewrote the merge to push each cloud setting_id and each LocalPreset row
  as its own FilamentOption with the full @printer suffix preserved in
  displayName. Built-in dedup against cloud setting_id is kept (mirrors
  ConfigureAmsSlotModal.tsx). Wired api.getBuiltinFilaments() into both
  callers. slicer_filament persistence is unchanged so existing spools
  keep slicing correctly.
maziggy 2 weeks ago
parent
commit
30fe88a334

File diff suppressed because it is too large
+ 0 - 0
CHANGELOG.md


+ 1 - 0
frontend/src/__tests__/components/SpoolFormBulk.test.tsx

@@ -31,6 +31,7 @@ vi.mock('../../api/client', () => ({
     getSpoolCatalog: vi.fn().mockResolvedValue([]),
     getSpoolCatalog: vi.fn().mockResolvedValue([]),
     getColorCatalog: vi.fn().mockResolvedValue([]),
     getColorCatalog: vi.fn().mockResolvedValue([]),
     getLocalPresets: vi.fn().mockResolvedValue({ filament: [] }),
     getLocalPresets: vi.fn().mockResolvedValue({ filament: [] }),
+    getBuiltinFilaments: vi.fn().mockResolvedValue([]),
     getPrinters: vi.fn().mockResolvedValue([]),
     getPrinters: vi.fn().mockResolvedValue([]),
     getSpoolUsageHistory: vi.fn().mockResolvedValue([]),
     getSpoolUsageHistory: vi.fn().mockResolvedValue([]),
     createSpool: vi.fn().mockResolvedValue({ id: 99 }),
     createSpool: vi.fn().mockResolvedValue({ id: 99 }),

+ 1 - 0
frontend/src/__tests__/components/SpoolFormModal.test.tsx

@@ -23,6 +23,7 @@ vi.mock('../../api/client', () => ({
     getSpoolCatalog: vi.fn().mockResolvedValue([]),
     getSpoolCatalog: vi.fn().mockResolvedValue([]),
     getColorCatalog: vi.fn().mockResolvedValue([]),
     getColorCatalog: vi.fn().mockResolvedValue([]),
     getLocalPresets: vi.fn().mockResolvedValue({ filament: [] }),
     getLocalPresets: vi.fn().mockResolvedValue({ filament: [] }),
+    getBuiltinFilaments: vi.fn().mockResolvedValue([]),
     getPrinters: vi.fn().mockResolvedValue([]),
     getPrinters: vi.fn().mockResolvedValue([]),
     getSpoolUsageHistory: vi.fn().mockResolvedValue([]),
     getSpoolUsageHistory: vi.fn().mockResolvedValue([]),
     createSpool: vi.fn().mockResolvedValue({ id: 99 }),
     createSpool: vi.fn().mockResolvedValue({ id: 99 }),

+ 162 - 0
frontend/src/__tests__/components/spool-form/buildFilamentOptions.test.ts

@@ -0,0 +1,162 @@
+/**
+ * Regression tests for buildFilamentOptions (#1248).
+ *
+ * The original bug was precedence-based merging: cloud presets, when present,
+ * fully replaced the local-presets branch and silently hid Local Profiles.
+ *
+ * The follow-up clarification: the spool form is printer-agnostic, so it must
+ * show every per-printer / per-nozzle variant of a preset as its own entry —
+ * unlike the AMS Slot modal which is per-printer and filters down to the
+ * active printer model. Both surfaces should render the same set of presets
+ * if you summed the AMS Slot's per-printer-filtered output across all
+ * printers; the spool form just shows the union directly.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { buildFilamentOptions } from '../../../components/spool-form/utils';
+import type { SlicerSetting, LocalPreset, BuiltinFilament } from '../../../api/client';
+
+const cloudPreset = (overrides: Partial<SlicerSetting> = {}): SlicerSetting => ({
+  setting_id: 'GFSL00@P1S',
+  name: 'Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle',
+  type: 'filament',
+  version: null,
+  user_id: null,
+  updated_time: null,
+  is_custom: false,
+  ...overrides,
+});
+
+const localPreset = (overrides: Partial<LocalPreset> = {}): LocalPreset => ({
+  id: 1,
+  name: 'My Custom PETG @Bambu Lab P2S 0.4 nozzle',
+  preset_type: 'filament',
+  source: 'local',
+  filament_type: 'GFG00',
+  filament_vendor: 'Acme',
+  nozzle_temp_min: 230,
+  nozzle_temp_max: 260,
+  pressure_advance: null,
+  default_filament_colour: null,
+  filament_cost: null,
+  filament_density: null,
+  compatible_printers: null,
+  inherits: null,
+  version: null,
+  created_at: '2026-01-01T00:00:00Z',
+  updated_at: '2026-01-01T00:00:00Z',
+  ...overrides,
+});
+
+const builtin = (overrides: Partial<BuiltinFilament> = {}): BuiltinFilament => ({
+  filament_id: 'GFA00',
+  name: 'Bambu ASA Basic',
+  ...overrides,
+});
+
+describe('buildFilamentOptions', () => {
+  it('returns one entry per cloud setting_id (no @printer collapse)', () => {
+    const options = buildFilamentOptions(
+      [
+        cloudPreset({ setting_id: 'GFSL00@P1S', name: 'Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle' }),
+        cloudPreset({ setting_id: 'GFSL00@X1C', name: 'Bambu PLA Basic @Bambu Lab X1C 0.4 nozzle' }),
+        cloudPreset({ setting_id: 'GFSL00@A1', name: 'Bambu PLA Basic @Bambu Lab A1 0.4 nozzle' }),
+      ],
+      new Set(),
+    );
+    expect(options).toHaveLength(3);
+    expect(options.map(o => o.code)).toEqual([
+      'GFSL00@A1',
+      'GFSL00@P1S',
+      'GFSL00@X1C',
+    ]);
+  });
+
+  it('keeps the @printer suffix in displayName so users can tell variants apart', () => {
+    const options = buildFilamentOptions(
+      [cloudPreset({ name: 'Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle' })],
+      new Set(),
+    );
+    expect(options[0].displayName).toBe('Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle');
+  });
+
+  it('merges local profiles even when cloud has presets (#1248 regression)', () => {
+    const options = buildFilamentOptions(
+      [cloudPreset()],
+      new Set(),
+      [localPreset()],
+    );
+    const names = options.map(o => o.name);
+    expect(names).toContain('Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle');
+    expect(names).toContain('My Custom PETG @Bambu Lab P2S 0.4 nozzle');
+  });
+
+  it('merges built-in filaments alongside cloud and local sources', () => {
+    const options = buildFilamentOptions(
+      [cloudPreset()],
+      new Set(),
+      [localPreset()],
+      [builtin()],
+    );
+    const names = options.map(o => o.name);
+    expect(names).toContain('Bambu PLA Basic @Bambu Lab P1S 0.4 nozzle');
+    expect(names).toContain('My Custom PETG @Bambu Lab P2S 0.4 nozzle');
+    expect(names).toContain('Bambu ASA Basic');
+  });
+
+  it('lists each local preset individually (no @printer collapse)', () => {
+    const options = buildFilamentOptions(
+      [],
+      new Set(),
+      [
+        localPreset({ id: 1, name: 'My PETG @Bambu Lab P2S 0.4 nozzle' }),
+        localPreset({ id: 2, name: 'My PETG @Bambu Lab X1C 0.4 nozzle' }),
+        localPreset({ id: 3, name: 'My PETG @Bambu Lab P2S 0.6 nozzle' }),
+      ],
+    );
+    expect(options).toHaveLength(3);
+    expect(options.map(o => o.name).sort()).toEqual([
+      'My PETG @Bambu Lab P2S 0.4 nozzle',
+      'My PETG @Bambu Lab P2S 0.6 nozzle',
+      'My PETG @Bambu Lab X1C 0.4 nozzle',
+    ]);
+  });
+
+  it('local-preset allCodes carries both filament_type and the row id for findPresetOption', () => {
+    const options = buildFilamentOptions(
+      [],
+      new Set(),
+      [localPreset({ id: 42, filament_type: 'GFL00' })],
+    );
+    expect(options[0].allCodes).toEqual(expect.arrayContaining(['GFL00', '42']));
+  });
+
+  it('falls back to hardcoded list only when every source is empty', () => {
+    const options = buildFilamentOptions([], new Set(), [], []);
+    expect(options.length).toBeGreaterThan(0);
+    expect(options.map(o => o.name)).toContain('Generic PLA');
+  });
+
+  it('skips a built-in whose setting_id is already covered by cloud', () => {
+    const options = buildFilamentOptions(
+      [cloudPreset({ setting_id: 'GFSA00', name: 'Bambu ASA Basic' })],
+      new Set(),
+      undefined,
+      [builtin()],
+    );
+    const asaCount = options.map(o => o.name).filter(n => n === 'Bambu ASA Basic').length;
+    expect(asaCount).toBe(1);
+  });
+
+  it('result is sorted alphabetically by displayName', () => {
+    const options = buildFilamentOptions(
+      [],
+      new Set(),
+      [
+        localPreset({ id: 1, name: 'Zebra PLA' }),
+        localPreset({ id: 2, name: 'Alpha PLA' }),
+      ],
+    );
+    expect(options.map(o => o.name)).toEqual(['Alpha PLA', 'Zebra PLA']);
+  });
+});

+ 7 - 3
frontend/src/components/SpoolFormModal.tsx

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { useTranslation } from 'react-i18next';
 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, Unlink } from 'lucide-react';
 import { api, ApiError } from '../api/client';
 import { api, ApiError } from '../api/client';
-import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset, SpoolmanBulkCreateResult, SpoolKProfileInput, SpoolmanFilamentEntry } from '../api/client';
+import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset, BuiltinFilament, SpoolmanBulkCreateResult, SpoolKProfileInput, SpoolmanFilamentEntry } from '../api/client';
 import { Button } from './Button';
 import { Button } from './Button';
 import { useToast } from '../contexts/ToastContext';
 import { useToast } from '../contexts/ToastContext';
 import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
 import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
@@ -71,6 +71,9 @@ export function SpoolFormModal({
   // Local presets (OrcaSlicer imports)
   // Local presets (OrcaSlicer imports)
   const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);
   const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);
 
 
+  // Built-in filaments (static fallback)
+  const [builtinFilaments, setBuiltinFilaments] = useState<BuiltinFilament[]>([]);
+
   // Color catalog
   // Color catalog
   const [colorCatalog, setColorCatalog] = useState<{ manufacturer: string; color_name: string; hex_color: string; material: string | null }[]>([]);
   const [colorCatalog, setColorCatalog] = useState<{ manufacturer: string; color_name: string; hex_color: string; material: string | null }[]>([]);
 
 
@@ -130,6 +133,7 @@ export function SpoolFormModal({
       }
       }
       api.getColorCatalog().then(setColorCatalog).catch(console.error);
       api.getColorCatalog().then(setColorCatalog).catch(console.error);
       api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(console.error);
       api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(console.error);
+      api.getBuiltinFilaments().then(setBuiltinFilaments).catch(console.error);
 
 
       // Fetch printer calibrations if not provided via props
       // Fetch printer calibrations if not provided via props
       if (printersWithCalibrations.length === 0) {
       if (printersWithCalibrations.length === 0) {
@@ -182,8 +186,8 @@ export function SpoolFormModal({
 
 
   // Build filament options: cloud → local → fallback
   // Build filament options: cloud → local → fallback
   const filamentOptions = useMemo(
   const filamentOptions = useMemo(
-    () => buildFilamentOptions(cloudPresets, new Set(), localPresets),
-    [cloudPresets, localPresets],
+    () => buildFilamentOptions(cloudPresets, new Set(), localPresets, builtinFilaments),
+    [cloudPresets, localPresets, builtinFilaments],
   );
   );
 
 
   // Extract brands from presets
   // Extract brands from presets

+ 1 - 1
frontend/src/components/spool-form/FilamentSection.tsx

@@ -164,7 +164,7 @@ export function FilamentSection({
                 ) : (
                 ) : (
                   filteredPresets.map(option => (
                   filteredPresets.map(option => (
                     <button
                     <button
-                      key={option.code}
+                      key={`${option.code}::${option.name}`}
                       type="button"
                       type="button"
                       className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary truncate ${
                       className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary truncate ${
                         selectedPresetOption?.code === option.code
                         selectedPresetOption?.code === option.code

+ 96 - 70
frontend/src/components/spool-form/utils.ts

@@ -1,4 +1,4 @@
-import type { SlicerSetting, LocalPreset } from '../../api/client';
+import type { SlicerSetting, LocalPreset, BuiltinFilament } from '../../api/client';
 import type { ColorPreset, FilamentOption } from './types';
 import type { ColorPreset, FilamentOption } from './types';
 import { KNOWN_VARIANTS, DEFAULT_BRANDS, RECENT_COLORS_KEY, MAX_RECENT_COLORS } from './constants';
 import { KNOWN_VARIANTS, DEFAULT_BRANDS, RECENT_COLORS_KEY, MAX_RECENT_COLORS } from './constants';
 
 
@@ -119,92 +119,118 @@ export function extractBrandsFromPresets(presets: SlicerSetting[], localPresets?
   return Array.from(brandSet).sort((a, b) => a.localeCompare(b));
   return Array.from(brandSet).sort((a, b) => a.localeCompare(b));
 }
 }
 
 
-// Build filament options from local presets (OrcaSlicer imports)
+// Build filament options from local presets (OrcaSlicer / BambuStudio imports).
+// Each preset gets its own entry — no base-name collapse — so the spool form
+// shows all per-printer/per-nozzle variants the user has imported. The spool
+// itself is printer-agnostic, so the variant the user picks just becomes the
+// stored slicer_filament code (consumed by normalize_slicer_filament during
+// slicing — kept as preset.filament_type when available so the existing
+// "GFL05"-style normalisation still resolves).
 function buildLocalFilamentOptions(localPresets: LocalPreset[]): FilamentOption[] {
 function buildLocalFilamentOptions(localPresets: LocalPreset[]): FilamentOption[] {
   const filamentPresets = localPresets.filter(p => p.preset_type === 'filament');
   const filamentPresets = localPresets.filter(p => p.preset_type === 'filament');
   if (filamentPresets.length === 0) return [];
   if (filamentPresets.length === 0) return [];
 
 
-  const presetsMap = new Map<string, FilamentOption>();
-  for (const preset of filamentPresets) {
-    const baseName = preset.name.replace(/@.*$/, '').trim();
-    const existing = presetsMap.get(baseName);
-    if (existing) {
-      existing.allCodes.push(String(preset.id));
-    } else {
-      // Use filament_type as the code if available (e.g. "GFL00"), otherwise use the id
-      const code = preset.filament_type || String(preset.id);
-      presetsMap.set(baseName, {
-        code,
-        name: baseName,
-        displayName: baseName,
-        isCustom: false,
-        allCodes: [code],
-      });
-    }
-  }
-  return Array.from(presetsMap.values()).sort((a, b) => a.displayName.localeCompare(b.displayName));
+  const options: FilamentOption[] = filamentPresets.map(preset => {
+    const code = preset.filament_type || String(preset.id);
+    // allCodes carries every shape an existing saved spool might have stored
+    // for this preset (filament_type and the row id), so findPresetOption
+    // resolves both old and new picks.
+    const allCodes = Array.from(new Set([code, String(preset.id)]));
+    return {
+      code,
+      name: preset.name,
+      displayName: preset.name,
+      isCustom: false,
+      allCodes,
+    };
+  });
+  return options.sort((a, b) => a.displayName.localeCompare(b.displayName));
 }
 }
 
 
-// Build filament options: cloud presets → local presets → hardcoded fallback
+// Build filament options by merging cloud presets, local profiles, and built-in
+// filaments — matching the behavior of ConfigureAmsSlotModal and the wiki's
+// "Where Presets Come From" section. Earlier versions were precedence-based
+// (cloud-only when cloud had any presets), which silently hid Local Profiles
+// from users logged into Bambu Cloud — see #1248.
 export function buildFilamentOptions(
 export function buildFilamentOptions(
   cloudPresets: SlicerSetting[],
   cloudPresets: SlicerSetting[],
   configuredPrinterModels: Set<string>,
   configuredPrinterModels: Set<string>,
   localPresets?: LocalPreset[],
   localPresets?: LocalPreset[],
+  builtinFilaments?: BuiltinFilament[],
 ): FilamentOption[] {
 ): FilamentOption[] {
-  // 1. Cloud presets (highest priority)
-  if (cloudPresets.length > 0) {
-    const customPresets: FilamentOption[] = [];
-    const defaultPresetsMap = new Map<string, FilamentOption>();
-
-    for (const preset of cloudPresets) {
-      if (preset.is_custom) {
-        // Custom presets: include if matches configured printers or no printer filter
-        const presetNameUpper = preset.name.toUpperCase();
-        const matchesPrinter = configuredPrinterModels.size === 0 ||
-          Array.from(configuredPrinterModels).some(model => presetNameUpper.includes(model)) ||
-          !presetNameUpper.includes('@');
-
-        if (matchesPrinter) {
-          customPresets.push({
-            code: preset.setting_id,
-            name: preset.name,
-            displayName: `${preset.name} (Custom)`,
-            isCustom: true,
-            allCodes: [preset.setting_id],
-          });
-        }
-      } else {
-        // Default presets: deduplicate by base name
-        const baseName = preset.name.replace(/@.*$/, '').trim();
-        const existing = defaultPresetsMap.get(baseName);
-        if (existing) {
-          existing.allCodes.push(preset.setting_id);
-        } else {
-          defaultPresetsMap.set(baseName, {
-            code: preset.setting_id,
-            name: baseName,
-            displayName: baseName,
-            isCustom: false,
-            allCodes: [preset.setting_id],
-          });
-        }
+  const customPresets: FilamentOption[] = [];
+  const defaultPresets: FilamentOption[] = [];
+  const cloudCodes = new Set<string>();
+
+  // 1. Cloud presets — each setting_id gets its own entry. The spool form is
+  // printer-agnostic so we deliberately do NOT collapse "@P1S" / "@X1C"
+  // variants into a single row; the user picks the variant they want and
+  // its setting_id is what gets persisted.
+  for (const preset of cloudPresets) {
+    if (preset.is_custom) {
+      const presetNameUpper = preset.name.toUpperCase();
+      const matchesPrinter = configuredPrinterModels.size === 0 ||
+        Array.from(configuredPrinterModels).some(model => presetNameUpper.includes(model)) ||
+        !presetNameUpper.includes('@');
+
+      if (matchesPrinter) {
+        customPresets.push({
+          code: preset.setting_id,
+          name: preset.name,
+          displayName: `${preset.name} (Custom)`,
+          isCustom: true,
+          allCodes: [preset.setting_id],
+        });
+        cloudCodes.add(preset.setting_id);
       }
       }
+    } else {
+      defaultPresets.push({
+        code: preset.setting_id,
+        name: preset.name,
+        displayName: preset.name,
+        isCustom: false,
+        allCodes: [preset.setting_id],
+      });
+      cloudCodes.add(preset.setting_id);
     }
     }
-
-    return [
-      ...customPresets,
-      ...Array.from(defaultPresetsMap.values()),
-    ].sort((a, b) => a.displayName.localeCompare(b.displayName));
   }
   }
 
 
-  // 2. Local presets (OrcaSlicer imports)
-  if (localPresets && localPresets.length > 0) {
-    const localOptions = buildLocalFilamentOptions(localPresets);
-    if (localOptions.length > 0) return localOptions;
+  // 2. Local profiles (OrcaSlicer / BambuStudio imports)
+  const localOptions = localPresets && localPresets.length > 0
+    ? buildLocalFilamentOptions(localPresets)
+    : [];
+
+  // 3. Built-in filaments — only those not already represented by a cloud preset.
+  // Cloud setting_ids look like "GFSA00", built-in filament_ids look like "GFA00";
+  // map between the two so we don't render the same filament twice.
+  const builtinOptions: FilamentOption[] = [];
+  if (builtinFilaments && builtinFilaments.length > 0) {
+    for (const bf of builtinFilaments) {
+      const settingId = bf.filament_id.startsWith('GF')
+        ? 'GFS' + bf.filament_id.slice(2)
+        : bf.filament_id;
+      if (cloudCodes.has(bf.filament_id) || cloudCodes.has(settingId)) continue;
+      builtinOptions.push({
+        code: bf.filament_id,
+        name: bf.name,
+        displayName: bf.name,
+        isCustom: false,
+        allCodes: [bf.filament_id, settingId],
+      });
+    }
   }
   }
 
 
-  // 3. Hardcoded fallback
-  return FALLBACK_PRESETS;
+  const merged = [
+    ...customPresets,
+    ...defaultPresets,
+    ...localOptions,
+    ...builtinOptions,
+  ];
+
+  // 4. Hardcoded fallback only when literally every source is empty.
+  if (merged.length === 0) return FALLBACK_PRESETS;
+
+  return merged.sort((a, b) => a.displayName.localeCompare(b.displayName));
 }
 }
 
 
 // Find selected preset option
 // Find selected preset option

+ 5 - 2
frontend/src/pages/spoolbuddy/SpoolBuddyWriteTagPage.tsx

@@ -7,6 +7,7 @@ import type { SpoolBuddyOutletContext } from '../../components/spoolbuddy/SpoolB
 import {
 import {
   api,
   api,
   spoolbuddyApi,
   spoolbuddyApi,
+  type BuiltinFilament,
   type InventorySpool,
   type InventorySpool,
   type LocalPreset,
   type LocalPreset,
   type SlicerSetting,
   type SlicerSetting,
@@ -412,6 +413,7 @@ function NewSpoolTouchForm({ currencySymbol, onCreated, selectedSpool, t }: {
   const [loadingCloudPresets, setLoadingCloudPresets] = useState(false);
   const [loadingCloudPresets, setLoadingCloudPresets] = useState(false);
   const [cloudPresets, setCloudPresets] = useState<SlicerSetting[]>([]);
   const [cloudPresets, setCloudPresets] = useState<SlicerSetting[]>([]);
   const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);
   const [localPresets, setLocalPresets] = useState<LocalPreset[]>([]);
+  const [builtinFilaments, setBuiltinFilaments] = useState<BuiltinFilament[]>([]);
   const [spoolCatalog, setSpoolCatalog] = useState<SpoolCatalogEntry[]>([]);
   const [spoolCatalog, setSpoolCatalog] = useState<SpoolCatalogEntry[]>([]);
   const [colorCatalog, setColorCatalog] = useState<
   const [colorCatalog, setColorCatalog] = useState<
     { manufacturer: string; color_name: string; hex_color: string; material: string | null }[]
     { manufacturer: string; color_name: string; hex_color: string; material: string | null }[]
@@ -451,6 +453,7 @@ function NewSpoolTouchForm({ currencySymbol, onCreated, selectedSpool, t }: {
       api.getSpoolCatalog().then(setSpoolCatalog).catch(() => undefined);
       api.getSpoolCatalog().then(setSpoolCatalog).catch(() => undefined);
       api.getColorCatalog().then(setColorCatalog).catch(() => undefined);
       api.getColorCatalog().then(setColorCatalog).catch(() => undefined);
       api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(() => undefined);
       api.getLocalPresets().then(r => setLocalPresets(r.filament)).catch(() => undefined);
+      api.getBuiltinFilaments().then(setBuiltinFilaments).catch(() => undefined);
 
 
       try {
       try {
         const printers = await api.getPrinters();
         const printers = await api.getPrinters();
@@ -496,8 +499,8 @@ function NewSpoolTouchForm({ currencySymbol, onCreated, selectedSpool, t }: {
   }, [printersWithCalibrations]);
   }, [printersWithCalibrations]);
 
 
   const filamentOptions = useMemo(
   const filamentOptions = useMemo(
-    () => buildFilamentOptions(cloudPresets, new Set(), localPresets),
-    [cloudPresets, localPresets],
+    () => buildFilamentOptions(cloudPresets, new Set(), localPresets, builtinFilaments),
+    [cloudPresets, localPresets, builtinFilaments],
   );
   );
 
 
   const selectedPresetOption = useMemo(
   const selectedPresetOption = useMemo(

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CdUD30tt.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-W4CVK7gF.js"></script>
+    <script type="module" crossorigin src="/assets/index-CdUD30tt.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CHOG7xih.css">
     <link rel="stylesheet" crossorigin href="/assets/index-CHOG7xih.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff