Skip to content

Commit eab1f06

Browse files
committed
feat(ui): make save and clear-filters buttons state-aware
1 parent eafbc64 commit eab1f06

10 files changed

Lines changed: 292 additions & 27 deletions

File tree

client/src/pages/filaments/edit.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HttpError, useTranslate } from "@refinedev/core";
33
import { Alert, ColorPicker, DatePicker, Form, Input, InputNumber, message, Radio, Select, Typography } from "antd";
44
import TextArea from "antd/es/input/TextArea";
55
import dayjs from "dayjs";
6-
import { useEffect, useState } from "react";
6+
import { useEffect, useMemo, useState } from "react";
77
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
88
import { MultiColorPicker } from "../../components/multiColorPicker";
99
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
@@ -42,6 +42,7 @@ export const FilamentEdit = () => {
4242
optionLabel: "name",
4343
pagination: { mode: "off" },
4444
});
45+
const watchedAllValues = Form.useWatch([], formProps.form);
4546

4647
// Add the vendor_id field to the form
4748
if (formProps.initialValues) {
@@ -76,8 +77,71 @@ export const FilamentEdit = () => {
7677
}
7778
};
7879

80+
const normalizeForCompare = (value: unknown): unknown => {
81+
if (dayjs.isDayjs(value)) {
82+
return value.toISOString();
83+
}
84+
if (Array.isArray(value)) {
85+
return value.map(normalizeForCompare);
86+
}
87+
if (value && typeof value === "object") {
88+
const objectValue = value as Record<string, unknown>;
89+
return Object.keys(objectValue)
90+
.sort()
91+
.reduce<Record<string, unknown>>((acc, key) => {
92+
const normalizedValue = normalizeForCompare(objectValue[key]);
93+
if (normalizedValue !== undefined) {
94+
acc[key] = normalizedValue;
95+
}
96+
return acc;
97+
}, {});
98+
}
99+
return value;
100+
};
101+
102+
const toComparableState = (value: unknown): string => {
103+
const normalized = normalizeForCompare(value) as Record<string, unknown> | undefined;
104+
const normalizedExtra = { ...(normalized?.extra as Record<string, unknown> | undefined) };
105+
106+
return JSON.stringify({
107+
name: normalized?.name ?? "",
108+
vendor_id: normalized?.vendor_id ?? null,
109+
material: normalized?.material ?? "",
110+
price: normalized?.price ?? null,
111+
density: normalized?.density ?? null,
112+
diameter: normalized?.diameter ?? null,
113+
weight: normalized?.weight ?? null,
114+
spool_weight: normalized?.spool_weight ?? null,
115+
settings_extruder_temp: normalized?.settings_extruder_temp ?? null,
116+
settings_bed_temp: normalized?.settings_bed_temp ?? null,
117+
article_number: normalized?.article_number ?? "",
118+
external_id: normalized?.external_id ?? "",
119+
comment: normalized?.comment ?? "",
120+
color_hex: normalized?.color_hex ?? "",
121+
multi_color_direction: normalized?.multi_color_direction ?? "",
122+
multi_color_hexes: colorType === "single" ? "" : (normalized?.multi_color_hexes ?? ""),
123+
extra: normalizedExtra,
124+
});
125+
};
126+
127+
const initialComparableState = useMemo(
128+
() => (formProps.initialValues ? toComparableState(formProps.initialValues) : null),
129+
[formProps.initialValues, colorType],
130+
);
131+
const watchedComparableState = useMemo(
132+
() => (watchedAllValues ? toComparableState(watchedAllValues) : null),
133+
[watchedAllValues, colorType],
134+
);
135+
const hasFormChanges =
136+
initialComparableState !== null && watchedComparableState !== null && initialComparableState !== watchedComparableState;
137+
const saveButtonState = {
138+
...saveButtonProps,
139+
type: hasFormChanges ? ("primary" as const) : ("default" as const),
140+
disabled: saveButtonProps.disabled || !hasFormChanges,
141+
};
142+
79143
return (
80-
<Edit saveButtonProps={saveButtonProps}>
144+
<Edit saveButtonProps={saveButtonState}>
81145
{contextHolder}
82146
<Form {...formProps} layout="vertical">
83147
<Form.Item

client/src/pages/filaments/list.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
useSpoolmanMaterials,
2424
useSpoolmanVendors,
2525
} from "../../components/otherModels";
26-
import { removeUndefined } from "../../utils/filtering";
26+
import { hasMeaningfulFilters, removeUndefined } from "../../utils/filtering";
2727
import { EntityType, useGetFields } from "../../utils/queryFields";
2828
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
2929
import { useCurrencyFormatter } from "../../utils/settings";
@@ -164,13 +164,14 @@ export const FilamentList = () => {
164164
tableState,
165165
sorter: true,
166166
};
167+
const hasActiveFilters = hasMeaningfulFilters(filters);
167168

168169
return (
169170
<List
170171
headerButtons={({ defaultButtons }) => (
171172
<>
172173
<Button
173-
type="primary"
174+
type={hasActiveFilters ? "primary" : "default"}
174175
icon={<FilterOutlined />}
175176
onClick={() => {
176177
setFilters([], "replace");

client/src/pages/printing/printing.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,8 @@ export function useGetPrintSettings(): SpoolQRCodePrintSettings[] | undefined {
4444
});
4545
}
4646

47-
export function useSetPrintSettings(): (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => void {
48-
const mut = useSetSetting("print_presets");
49-
50-
return (spoolQRCodePrintSettings: SpoolQRCodePrintSettings[]) => {
51-
mut.mutate(spoolQRCodePrintSettings);
52-
};
47+
export function useSetPrintSettings() {
48+
return useSetSetting<SpoolQRCodePrintSettings[]>("print_presets");
5349
}
5450

5551
interface GenericObject {

client/src/pages/printing/spoolQrCodePrintingDialog.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CopyOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from "@ant-d
22
import { useTranslate } from "@refinedev/core";
33
import { Button, Flex, Form, Input, Modal, Popconfirm, Select, Table, Typography, message } from "antd";
44
import TextArea from "antd/es/input/TextArea";
5-
import { useState } from "react";
5+
import { useMemo, useState } from "react";
66
import { v4 as uuidv4 } from "uuid";
77
import { EntityType, useGetFields } from "../../utils/queryFields";
88
import { useGetSetting } from "../../utils/querySettings";
@@ -51,9 +51,19 @@ const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
5151

5252
const localOrRemotePresets = localPresets ?? remotePresets;
5353

54-
const savePresetsRemote = () => {
55-
if (!localPresets) return;
56-
setRemotePresets(localPresets);
54+
const remotePresetsComparable = useMemo(() => JSON.stringify(remotePresets ?? []), [remotePresets]);
55+
const localPresetsComparable = useMemo(
56+
() => JSON.stringify((localPresets ?? remotePresets) ?? []),
57+
[localPresets, remotePresets],
58+
);
59+
const hasUnsavedPresetChanges =
60+
localPresets !== undefined && localPresetsComparable !== remotePresetsComparable;
61+
62+
const savePresetsRemote = async (): Promise<boolean> => {
63+
if (!localPresets || !hasUnsavedPresetChanges) return false;
64+
await setRemotePresets.mutateAsync(localPresets);
65+
setLocalPresets(undefined);
66+
return true;
5767
};
5868

5969
// Functions to update settings
@@ -347,12 +357,21 @@ Spool Weight: {filament.spool_weight} g
347357
extraButtons={
348358
<>
349359
<Button
350-
type="primary"
360+
type={hasUnsavedPresetChanges ? "primary" : "default"}
351361
size="large"
352362
icon={<SaveOutlined />}
353-
onClick={() => {
354-
savePresetsRemote();
355-
messageApi.success(t("notifications.saveSuccessful"));
363+
loading={setRemotePresets.isPending}
364+
disabled={!hasUnsavedPresetChanges || setRemotePresets.isPending}
365+
onClick={async () => {
366+
try {
367+
const wasSaved = await savePresetsRemote();
368+
if (wasSaved) {
369+
messageApi.success(t("notifications.saveSuccessful"));
370+
}
371+
} catch (error) {
372+
const fallback = t("notifications.error", { statusCode: "unknown" });
373+
messageApi.error(error instanceof Error ? error.message : fallback);
374+
}
356375
}}
357376
>
358377
{t("printing.generic.saveSetting")}

client/src/pages/settings/generalSettings.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useTranslate } from "@refinedev/core";
22
import { Button, Checkbox, Form, Input, message } from "antd";
3-
import { useEffect } from "react";
3+
import { useEffect, useMemo } from "react";
44
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
55

66
export function GeneralSettings() {
@@ -9,6 +9,7 @@ export function GeneralSettings() {
99
const setCurrency = useSetSetting("currency");
1010
const setRoundPrices = useSetSetting("round_prices");
1111
const [form] = Form.useForm();
12+
const watchedAllValues = Form.useWatch([], form);
1213
const [messageApi, contextHolder] = message.useMessage();
1314
const t = useTranslate();
1415

@@ -47,6 +48,32 @@ export function GeneralSettings() {
4748
}
4849
};
4950

51+
const initialComparableState = useMemo(() => {
52+
if (!settings.data) {
53+
return null;
54+
}
55+
return JSON.stringify({
56+
currency: JSON.parse(settings.data.currency.value),
57+
base_url: JSON.parse(settings.data.base_url.value),
58+
round_prices: JSON.parse(settings.data.round_prices.value),
59+
});
60+
}, [settings.data]);
61+
62+
const watchedComparableState = useMemo(() => {
63+
if (!watchedAllValues) {
64+
return null;
65+
}
66+
return JSON.stringify({
67+
currency: watchedAllValues.currency ?? "",
68+
base_url: watchedAllValues.base_url ?? "",
69+
round_prices: watchedAllValues.round_prices ?? false,
70+
});
71+
}, [watchedAllValues]);
72+
73+
const hasFormChanges =
74+
initialComparableState !== null && watchedComparableState !== null && initialComparableState !== watchedComparableState;
75+
const isSaving = settings.isFetching || setCurrency.isPending || setBaseUrl.isPending || setRoundPrices.isPending;
76+
5077
return (
5178
<>
5279
<Form
@@ -105,7 +132,12 @@ export function GeneralSettings() {
105132
</Form.Item>
106133

107134
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
108-
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isPending}>
135+
<Button
136+
type={hasFormChanges ? "primary" : "default"}
137+
htmlType="submit"
138+
loading={isSaving}
139+
disabled={isSaving || !hasFormChanges}
140+
>
109141
{t("buttons.save")}
110142
</Button>
111143
</Form.Item>

client/src/pages/spools/edit.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const SpoolEdit = () => {
9797
return null;
9898
}
9999
}, [selectedFilamentID, internalSelectOptions, externalSelectOptions]);
100+
const watchedAllValues = Form.useWatch([], form);
100101

101102
// Override the form's onFinish method to stringify the extra fields
102103
const originalOnFinish = formProps.onFinish;
@@ -230,8 +231,65 @@ export const SpoolEdit = () => {
230231
}
231232
}, [initialUsedWeight]);
232233

234+
const normalizeForCompare = (value: unknown): unknown => {
235+
if (dayjs.isDayjs(value)) {
236+
return value.toISOString();
237+
}
238+
if (Array.isArray(value)) {
239+
return value.map(normalizeForCompare);
240+
}
241+
if (value && typeof value === "object") {
242+
const objectValue = value as Record<string, unknown>;
243+
return Object.keys(objectValue)
244+
.sort()
245+
.reduce<Record<string, unknown>>((acc, key) => {
246+
const normalizedValue = normalizeForCompare(objectValue[key]);
247+
if (normalizedValue !== undefined) {
248+
acc[key] = normalizedValue;
249+
}
250+
return acc;
251+
}, {});
252+
}
253+
return value;
254+
};
255+
256+
const toComparableState = (value: unknown): string => {
257+
const normalized = normalizeForCompare(value) as Record<string, unknown> | undefined;
258+
const normalizedExtra = { ...(normalized?.extra as Record<string, unknown> | undefined) };
259+
260+
return JSON.stringify({
261+
first_used: normalized?.first_used ?? null,
262+
last_used: normalized?.last_used ?? null,
263+
filament_id: normalized?.filament_id ?? null,
264+
price: normalized?.price ?? null,
265+
initial_weight: normalized?.initial_weight ?? null,
266+
spool_weight: normalized?.spool_weight ?? null,
267+
used_weight: normalized?.used_weight ?? null,
268+
location: normalized?.location ?? "",
269+
lot_nr: normalized?.lot_nr ?? "",
270+
comment: normalized?.comment ?? "",
271+
extra: normalizedExtra,
272+
});
273+
};
274+
275+
const initialComparableState = useMemo(
276+
() => (formProps.initialValues ? toComparableState(formProps.initialValues) : null),
277+
[formProps.initialValues],
278+
);
279+
const watchedComparableState = useMemo(
280+
() => (watchedAllValues ? toComparableState(watchedAllValues) : null),
281+
[watchedAllValues],
282+
);
283+
const hasFormChanges =
284+
initialComparableState !== null && watchedComparableState !== null && initialComparableState !== watchedComparableState;
285+
const saveButtonState = {
286+
...saveButtonProps,
287+
type: hasFormChanges ? ("primary" as const) : ("default" as const),
288+
disabled: saveButtonProps.disabled || !hasFormChanges,
289+
};
290+
233291
return (
234-
<Edit saveButtonProps={saveButtonProps}>
292+
<Edit saveButtonProps={saveButtonState}>
235293
{contextHolder}
236294
<Form {...formProps} layout="vertical">
237295
<Form.Item

client/src/pages/spools/list.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
useSpoolmanLotNumbers,
3434
useSpoolmanMaterials,
3535
} from "../../components/otherModels";
36-
import { removeUndefined } from "../../utils/filtering";
36+
import { hasMeaningfulFilters, removeUndefined } from "../../utils/filtering";
3737
import { EntityType, useGetFields } from "../../utils/queryFields";
3838
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
3939
import { useCurrencyFormatter } from "../../utils/settings";
@@ -255,6 +255,7 @@ export const SpoolList = () => {
255255
tableState,
256256
sorter: true,
257257
};
258+
const hasActiveFilters = hasMeaningfulFilters(filters);
258259

259260
return (
260261
<List
@@ -279,7 +280,7 @@ export const SpoolList = () => {
279280
{showArchived ? t("buttons.hideArchived") : t("buttons.showArchived")}
280281
</Button>
281282
<Button
282-
type="primary"
283+
type={hasActiveFilters ? "primary" : "default"}
283284
icon={<FilterOutlined />}
284285
onClick={() => {
285286
setFilters([], "replace");

0 commit comments

Comments
 (0)