Skip to content
86 changes: 76 additions & 10 deletions client/src/pages/filaments/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { HttpError, useTranslate } from "@refinedev/core";
import { Alert, ColorPicker, DatePicker, Form, Input, InputNumber, message, Radio, Select, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { MultiColorPicker } from "../../components/multiColorPicker";
import { toComparableState } from "../../utils/formState";
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { IVendor } from "../vendors/model";
import { IFilament, IFilamentParsedExtras } from "./model";
import { IFilament, IFilamentEditForm, IFilamentParsedExtras } from "./model";

/*
The API returns the extra fields as JSON values, but we need to parse them into their real types
Expand All @@ -19,6 +20,29 @@ We also need to stringify them again before sending them back to the API, which
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
*/

// comparableDefaults is typed against IFilamentEditForm so TypeScript will report a compile
// error here if a new editable field is added to the model without updating this list.
const comparableDefaults: Record<keyof IFilamentEditForm, unknown> = {
name: "",
vendor_id: null,
material: "",
price: null,
density: null,
diameter: null,
weight: null,
spool_weight: null,
settings_extruder_temp: null,
settings_bed_temp: null,
article_number: "",
external_id: "",
comment: "",
color_hex: "",
multi_color_direction: "",
multi_color_hexes: "",
extra: {},
};
// This list is the source of truth for which inputs participate in the Save-button dirty check.

export const FilamentEdit = () => {
const t = useTranslate();
const [messageApi, contextHolder] = message.useMessage();
Expand All @@ -42,14 +66,18 @@ export const FilamentEdit = () => {
optionLabel: "name",
pagination: { mode: "off" },
});
const watchedAllValues = Form.useWatch([], formProps.form);

// Add the vendor_id field to the form
if (formProps.initialValues) {
formProps.initialValues["vendor_id"] = formProps.initialValues["vendor"]?.id;

// Parse the extra fields from string values into real types
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
// Initialize form fields and parse extra fields
useEffect(() => {
if (formProps.initialValues && formProps.form) {
const parsed = ParsedExtras(formProps.initialValues);
formProps.form.setFieldsValue({
...parsed,
vendor_id: formProps.initialValues.vendor?.id,
} as unknown as IFilament);
}
}, [formProps.form, formProps.initialValues?.id, formProps.initialValues?.vendor?.id]);

// Update colorType state
useEffect(() => {
Expand All @@ -76,8 +104,46 @@ export const FilamentEdit = () => {
}
};

const initialComparableState = useMemo(
// ParsedExtras normalizes extra-field values from raw API JSON strings to their actual
// types so the initial snapshot matches the form state that setFieldsValue produces.
() =>
toComparableState(
formProps.initialValues
? {
...ParsedExtras(formProps.initialValues),
vendor_id: formProps.initialValues.vendor?.id ?? null,
}
: formProps.initialValues,
comparableDefaults,
{
// Single-color mode should ignore any dormant multi-color payload when deciding whether Save is needed.
multi_color_hexes: (normalized: Record<string, unknown>) =>
colorType === "single" ? "" : ((normalized.multi_color_hexes as string | undefined) ?? ""),
Comment on lines +121 to +122
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat color-mode toggle as a dirty form change

When colorType is switched to "single", this override forces multi_color_hexes to "" for both the initial snapshot and the live form snapshot. For filaments that start as multi-color, toggling to single-color without editing any other field leaves hasFormChanges false, so the Save button stays disabled even though submit logic would clear multi_color_hexes. This blocks a valid user action unless they also modify an unrelated field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this issue is unrelated to the changes this PR does on top of master right? this would be it's own PR to fix.

},
),
[formProps.initialValues, formProps.initialValues?.vendor?.id, colorType],
);
const watchedComparableState = useMemo(
() =>
toComparableState(watchedAllValues, comparableDefaults, {
multi_color_hexes: (normalized: Record<string, unknown>) =>
colorType === "single" ? "" : ((normalized.multi_color_hexes as string | undefined) ?? ""),
}),
[watchedAllValues, colorType],
);
const hasFormChanges =
initialComparableState !== null &&
watchedComparableState !== null &&
initialComparableState !== watchedComparableState;
const saveButtonState = {
...saveButtonProps,
type: hasFormChanges ? ("primary" as const) : ("default" as const),
disabled: saveButtonProps.disabled || !hasFormChanges,
};

return (
<Edit saveButtonProps={saveButtonProps}>
<Edit saveButtonProps={saveButtonState}>
{contextHolder}
<Form {...formProps} layout="vertical">
<Form.Item
Expand Down
6 changes: 4 additions & 2 deletions client/src/pages/filaments/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
useSpoolmanMaterials,
useSpoolmanVendors,
} from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";
import { hasMeaningfulFilters, removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { useCurrencyFormatter } from "../../utils/settings";
Expand Down Expand Up @@ -164,13 +164,15 @@ export const FilamentList = () => {
tableState,
sorter: true,
};
// Ignore empty filter shells so the Clear Filters button only lights up for filters that would affect results.
const hasActiveFilters = hasMeaningfulFilters(filters);

return (
<List
headerButtons={({ defaultButtons }) => (
<>
<Button
type="primary"
type={hasActiveFilters ? "primary" : "default"}
icon={<FilterOutlined />}
onClick={() => {
setFilters([], "replace");
Expand Down
8 changes: 8 additions & 0 deletions client/src/pages/filaments/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ export interface IFilament {

// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
export type IFilamentParsedExtras = Omit<IFilament, "extra"> & { extra?: { [key: string]: unknown } };

// IFilamentEditForm is the shape of the filament edit form — only user-editable fields.
// id and registered are system-managed; vendor is replaced by vendor_id.
// Adding a new editable field to IFilament should also be added here;
// comparableDefaults in filaments/edit.tsx is typed against this.
export type IFilamentEditForm = Omit<IFilamentParsedExtras, "id" | "registered" | "vendor"> & {
vendor_id: number | null;
};
27 changes: 14 additions & 13 deletions client/src/pages/printing/printing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,22 @@ export function useGetPrintSettings(): SpoolQRCodePrintSettings[] | undefined {
if (!data) return;
const parsed: SpoolQRCodePrintSettings[] =
data && data.value ? JSON.parse(data.value) : ([] as SpoolQRCodePrintSettings[]);
// Loop through all parsed and generate a new ID field if it's not set
return parsed.map((settings) => {
if (!settings.labelSettings.printSettings.id) {
settings.labelSettings.printSettings.id = uuidv4();
}
return settings;
});
// Keep preset loading immutable so dirty-state comparisons are not affected by
// IDs being injected directly into parsed settings objects.
return parsed.map((settings) => ({
...settings,
labelSettings: {
...settings.labelSettings,
printSettings: {
...settings.labelSettings.printSettings,
id: settings.labelSettings.printSettings.id || uuidv4(),
},
},
}));
}

export function useSetPrintSettings(): (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => void {
const mut = useSetSetting("print_presets");

return (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => {
mut.mutate(spoolQRCodePrintSettings);
};
export function useSetPrintSettings() {
return useSetSetting<SpoolQRCodePrintSettings[]>("print_presets");
}

interface GenericObject {
Expand Down
Loading