mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user