fix(inventory): allow editing and duplicating stock spools without a slicer preset (#1905)

A spool created by Quick Add, a CSV import or an RFID scan has no slicer
preset, brand or subtype. Reopening it in Edit Spool demanded all three
before anything could be saved, so changing its storage location, cost
or notes was impossible - and Copy Spool had the same gate with no Quick
Add toggle to waive it. The preset you were then forced to pick auto-
filled material, brand and subtype from the preset name, silently
rewriting a hand-entered manufacturer (Elegoo -> Generic) so the spool
no longer appeared where it had been filed.

Editing and copying now require only what the backend requires: the
material. Preset, brand and subtype stay fully visible and editable -
nothing is hidden the way Quick Add hides it - and the required-field
markers no longer advertise a rule that isn't enforced. Selecting a
preset fills only fields that are still empty or that a previously
selected preset had filled, so values the user (or the saved spool)
provided survive; switching between presets still replaces what the
earlier one contributed.

The brand and material dropdowns also no longer filter themselves down
to the brand/material pairs known to the color catalog and slicer
presets. Elegoo is catalogued only for PLA, which made a real product
like Elegoo ASA look impossible to enter. Both lists now always offer
everything known, with paired entries ranked first under Suggested and
the rest under All, and a spool's own custom brand or material is always
present in its own dropdown. The SpoolBuddy write-tag form shares these
fields and gets the same treatment.

Lastly the Quick Add layout no longer leaks out of create mode: quick-
adding a spool and then opening Edit left the edit form in the reduced
layout with no toggle to leave it, because the toggle is create-only.

Frontend only. Translated in all locales; wiki updated. Covered by
validation and form-interaction tests.
This commit is contained in:
maziggy
2026-07-28 13:09:16 +02:00
parent af7874546a
commit f4f76e0121
22 changed files with 716 additions and 219 deletions
+40 -20
View File
@@ -6,9 +6,9 @@ import { api, ApiError } from '../api/client';
import type { InventorySpool, SlicerSetting, SpoolCatalogEntry, LocalPreset, BuiltinFilament, SpoolmanBulkCreateResult, SpoolKProfileInput, SpoolmanFilamentEntry } from '../api/client';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
import type { SpoolFormData, PrinterWithCalibrations, ColorPreset, SpoolFormMode } from './spool-form/types';
import { defaultFormData, validateForm, SPOOLMAN_LINKED_FIELDS } from './spool-form/types';
import { buildFilamentOptions, extractBrandsFromPresets, fetchPrinterCalibrations, findPresetOption, loadRecentColors, parsePresetName, saveRecentColor } from './spool-form/utils';
import { buildFilamentOptions, extractBrandsFromPresets, fetchPrinterCalibrations, findPresetOption, loadRecentColors, pairedOptions, parsePresetName, saveRecentColor, withCurrentValue } from './spool-form/utils';
import { MATERIALS } from './spool-form/constants';
import { FilamentSection } from './spool-form/FilamentSection';
import { ColorSection } from './spool-form/ColorSection';
@@ -25,7 +25,7 @@ type TabId = 'filament' | 'pa-profile';
const CLEAR_TAG_PAYLOAD = { tag_uid: null, tray_uuid: null, tag_type: null, data_origin: null };
export type SpoolFormMode = 'create' | 'edit' | 'copy';
export type { SpoolFormMode };
interface SpoolFormModalProps {
isOpen: boolean;
@@ -300,21 +300,33 @@ export function SpoolFormModal({
return map;
}, [brandMaterialPairs]);
const availableBrands = useMemo(() => {
if (!formData.material) return baseAvailableBrands;
const materialKey = formData.material.toLowerCase();
const brandKeys = materialToBrands.get(materialKey);
if (!brandKeys || brandKeys.size === 0) return baseAvailableBrands;
return baseAvailableBrands.filter(brand => brandKeys.has(brand.toLowerCase()));
}, [baseAvailableBrands, formData.material, materialToBrands]);
// #1905: the brand and material dropdowns used to be filtered down to the
// pairs seen in the color catalog / slicer presets, which hid perfectly valid
// combinations — "Elegoo" exists (as a PLA brand) but vanished from the list
// once ASA was selected, making the entry look impossible. Both lists now
// always offer everything we know about, plus whatever the spool already has
// stored (a custom brand saved earlier was missing from its own dropdown).
// The pairing knowledge survives as `suggestedBrands`/`suggestedMaterials`,
// which the dropdowns sort to the top instead of filtering by.
const availableBrands = useMemo(
() => withCurrentValue(baseAvailableBrands, formData.brand),
[baseAvailableBrands, formData.brand],
);
const availableMaterials = useMemo(() => {
if (!formData.brand) return baseAvailableMaterials;
const brandKey = formData.brand.toLowerCase();
const materialKeys = brandToMaterials.get(brandKey);
if (!materialKeys || materialKeys.size === 0) return baseAvailableMaterials;
return baseAvailableMaterials.filter(material => materialKeys.has(material.toLowerCase()));
}, [baseAvailableMaterials, formData.brand, brandToMaterials]);
const availableMaterials = useMemo(
() => withCurrentValue(baseAvailableMaterials, formData.material),
[baseAvailableMaterials, formData.material],
);
const suggestedBrands = useMemo(
() => pairedOptions(availableBrands, formData.material, materialToBrands),
[availableBrands, formData.material, materialToBrands],
);
const suggestedMaterials = useMemo(
() => pairedOptions(availableMaterials, formData.brand, brandToMaterials),
[availableMaterials, formData.brand, brandToMaterials],
);
// Find selected preset option
const selectedPresetOption = useMemo(
@@ -378,9 +390,14 @@ export function SpoolFormModal({
setFormData(defaultFormData);
setPresetInputValue('');
setSelectedProfiles(new Set());
setQuickAdd(false);
setQuantity(1);
}
// Reset on every open, not just the create path (#1905). The modal keeps
// its state while closed, and the Quick Add toggle only renders in create
// mode — so quick-adding a spool and then opening Edit left the edit form
// stuck in quick-add layout (no preset field, no PA-profile tab) with no
// control to switch back.
setQuickAdd(false);
setQuantity(1);
setErrors({});
setActiveTab('filament');
setWeightTouched(false);
@@ -717,7 +734,7 @@ export function SpoolFormModal({
if (!isOpen) return null;
const handleSubmit = () => {
const validation = validateForm(formData, quickAdd, spoolmanMode);
const validation = validateForm(formData, quickAdd, spoolmanMode, mode);
if (!validation.isValid) {
setErrors(validation.errors);
if (validation.errors.slicer_filament || validation.errors.material || validation.errors.brand || validation.errors.subtype) {
@@ -896,7 +913,10 @@ export function SpoolFormModal({
filamentOptions={filamentOptions}
availableBrands={availableBrands}
availableMaterials={availableMaterials}
suggestedBrands={suggestedBrands}
suggestedMaterials={suggestedMaterials}
quickAdd={quickAdd}
detailsRequired={!quickAdd && !spoolmanMode && mode === 'create'}
quantity={quantity}
onQuantityChange={setQuantity}
errors={errors}