/ client / src / pages / filaments / edit.tsx
edit.tsx
  1  import { Edit, useForm, useSelect } from "@refinedev/antd";
  2  import { HttpError, useTranslate } from "@refinedev/core";
  3  import { Alert, ColorPicker, DatePicker, Form, Input, InputNumber, message, Radio, Select, Typography } from "antd";
  4  import TextArea from "antd/es/input/TextArea";
  5  import dayjs from "dayjs";
  6  import { useEffect, useState } from "react";
  7  import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
  8  import { MultiColorPicker } from "../../components/multiColorPicker";
  9  import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
 10  import { EntityType, useGetFields } from "../../utils/queryFields";
 11  import { getCurrencySymbol, useCurrency } from "../../utils/settings";
 12  import { IVendor } from "../vendors/model";
 13  import { IFilament, IFilamentParsedExtras } from "./model";
 14  
 15  /*
 16  The API returns the extra fields as JSON values, but we need to parse them into their real types
 17  in order for Ant design's form to work properly. ParsedExtras does this for us.
 18  We also need to stringify them again before sending them back to the API, which is done by overriding
 19  the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
 20  */
 21  
 22  export const FilamentEdit = () => {
 23    const t = useTranslate();
 24    const [messageApi, contextHolder] = message.useMessage();
 25    const [hasChanged, setHasChanged] = useState(false);
 26    const extraFields = useGetFields(EntityType.filament);
 27    const currency = useCurrency();
 28    const [colorType, setColorType] = useState<"single" | "multi">("single");
 29  
 30    const { formProps, saveButtonProps } = useForm<IFilament, HttpError, IFilament, IFilament>({
 31      liveMode: "manual",
 32      onLiveEvent() {
 33        // Warn the user if the filament has been updated since the form was opened
 34        messageApi.warning(t("filament.form.filament_updated"));
 35        setHasChanged(true);
 36      },
 37    });
 38  
 39    // Get vendor selection options
 40    const { selectProps } = useSelect<IVendor>({
 41      resource: "vendor",
 42      optionLabel: "name",
 43      pagination: { mode: "off" },
 44    });
 45  
 46    // Add the vendor_id field to the form
 47    if (formProps.initialValues) {
 48      formProps.initialValues["vendor_id"] = formProps.initialValues["vendor"]?.id;
 49  
 50      // Parse the extra fields from string values into real types
 51      formProps.initialValues = ParsedExtras(formProps.initialValues);
 52    }
 53  
 54    // Update colorType state
 55    useEffect(() => {
 56      if (formProps.initialValues?.multi_color_hexes) {
 57        setColorType("multi");
 58      } else {
 59        setColorType("single");
 60      }
 61    }, [formProps.initialValues?.multi_color_hexes]);
 62  
 63    // Override the form's onFinish method to stringify the extra fields
 64    const originalOnFinish = formProps.onFinish;
 65    formProps.onFinish = (allValues: IFilamentParsedExtras) => {
 66      if (allValues !== undefined && allValues !== null) {
 67        if (colorType == "single") {
 68          allValues.multi_color_hexes = "";
 69        }
 70        // Lot of stupidity here to make types work
 71        const stringifiedAllValues = StringifiedExtras<IFilamentParsedExtras>(allValues);
 72        originalOnFinish?.({
 73          extra: {},
 74          ...stringifiedAllValues,
 75        });
 76      }
 77    };
 78  
 79    return (
 80      <Edit saveButtonProps={saveButtonProps}>
 81        {contextHolder}
 82        <Form {...formProps} layout="vertical">
 83          <Form.Item
 84            label={t("filament.fields.id")}
 85            name={["id"]}
 86            rules={[
 87              {
 88                required: true,
 89              },
 90            ]}
 91          >
 92            <Input readOnly disabled />
 93          </Form.Item>
 94          <Form.Item
 95            label={t("filament.fields.registered")}
 96            name={["registered"]}
 97            rules={[
 98              {
 99                required: true,
100              },
101            ]}
102            getValueProps={(value) => ({
103              value: value ? dayjs(value) : undefined,
104            })}
105          >
106            <DatePicker disabled showTime format="YYYY-MM-DD HH:mm:ss" />
107          </Form.Item>
108          <Form.Item
109            label={t("filament.fields.name")}
110            help={t("filament.fields_help.name")}
111            name={["name"]}
112            rules={[
113              {
114                required: false,
115              },
116            ]}
117          >
118            <Input maxLength={64} />
119          </Form.Item>
120          <Form.Item
121            label={t("filament.fields.vendor")}
122            name={["vendor_id"]}
123            rules={[
124              {
125                required: false,
126              },
127            ]}
128            // Applying this to Form.Item Select's causes a cleared select to send null
129            normalize={(value) => {
130              if (value === undefined) {
131                return null;
132              }
133              return value;
134            }}
135          >
136            <Select
137              {...selectProps}
138              allowClear
139              filterSort={(a, b) => {
140                return a?.label && b?.label
141                  ? (a.label as string).localeCompare(b.label as string, undefined, { sensitivity: "base" })
142                  : 0;
143              }}
144              filterOption={(input, option) =>
145                typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
146              }
147            />
148          </Form.Item>
149          <Form.Item label={t("filament.fields.color_hex")}>
150            <Radio.Group
151              onChange={(value) => {
152                setColorType(value.target.value);
153              }}
154              defaultValue={colorType}
155              value={colorType}
156            >
157              <Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
158              <Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
159            </Radio.Group>
160          </Form.Item>
161          {colorType == "single" && (
162            <Form.Item
163              name={"color_hex"}
164              rules={[
165                {
166                  required: false,
167                },
168              ]}
169              getValueFromEvent={(e) => {
170                return e?.toHex();
171              }}
172            >
173              <ColorPicker />
174            </Form.Item>
175          )}
176          {colorType == "multi" && (
177            <Form.Item
178              name={"multi_color_direction"}
179              help={t("filament.fields_help.multi_color_direction")}
180              rules={[
181                {
182                  required: true,
183                },
184              ]}
185              initialValue={"coaxial"}
186            >
187              <Radio.Group>
188                <Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
189                <Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
190              </Radio.Group>
191            </Form.Item>
192          )}
193          {colorType == "multi" && (
194            <Form.Item
195              name={"multi_color_hexes"}
196              rules={[
197                {
198                  required: false,
199                },
200              ]}
201            >
202              <MultiColorPicker min={2} max={14} />
203            </Form.Item>
204          )}
205          <Form.Item
206            label={t("filament.fields.material")}
207            help={t("filament.fields_help.material")}
208            name={["material"]}
209            rules={[
210              {
211                required: false,
212              },
213            ]}
214          >
215            <Input maxLength={64} />
216          </Form.Item>
217          <Form.Item
218            label={t("filament.fields.price")}
219            help={t("filament.fields_help.price")}
220            name={["price"]}
221            rules={[
222              {
223                required: false,
224                type: "number",
225                min: 0,
226              },
227            ]}
228          >
229            <InputNumber
230              addonAfter={getCurrencySymbol(undefined, currency)}
231              precision={2}
232              formatter={formatNumberOnUserInput}
233              parser={numberParserAllowEmpty}
234            />
235          </Form.Item>
236          <Form.Item
237            label={t("filament.fields.density")}
238            name={["density"]}
239            rules={[
240              {
241                required: true,
242                type: "number",
243                min: 0,
244                max: 100,
245              },
246            ]}
247          >
248            <InputNumber addonAfter="g/cm³" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
249          </Form.Item>
250          <Form.Item
251            label={t("filament.fields.diameter")}
252            name={["diameter"]}
253            rules={[
254              {
255                required: true,
256                type: "number",
257                min: 0,
258                max: 10,
259              },
260            ]}
261          >
262            <InputNumber addonAfter="mm" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
263          </Form.Item>
264          <Form.Item
265            label={t("filament.fields.weight")}
266            help={t("filament.fields_help.weight")}
267            name={["weight"]}
268            rules={[
269              {
270                required: false,
271                type: "number",
272                min: 0,
273              },
274            ]}
275          >
276            <InputNumber addonAfter="g" precision={1} />
277          </Form.Item>
278          <Form.Item
279            label={t("filament.fields.spool_weight")}
280            help={t("filament.fields_help.spool_weight")}
281            name={["spool_weight"]}
282            rules={[
283              {
284                required: false,
285                type: "number",
286                min: 0,
287              },
288            ]}
289          >
290            <InputNumber addonAfter="g" precision={1} />
291          </Form.Item>
292          <Form.Item
293            label={t("filament.fields.settings_extruder_temp")}
294            name={["settings_extruder_temp"]}
295            rules={[
296              {
297                required: false,
298                type: "number",
299                min: 0,
300              },
301            ]}
302          >
303            <InputNumber addonAfter="°C" precision={0} />
304          </Form.Item>
305          <Form.Item
306            label={t("filament.fields.settings_bed_temp")}
307            name={["settings_bed_temp"]}
308            rules={[
309              {
310                required: false,
311                type: "number",
312                min: 0,
313              },
314            ]}
315          >
316            <InputNumber addonAfter="°C" precision={0} />
317          </Form.Item>
318          <Form.Item
319            label={t("filament.fields.article_number")}
320            help={t("filament.fields_help.article_number")}
321            name={["article_number"]}
322            rules={[
323              {
324                required: false,
325              },
326            ]}
327          >
328            <Input maxLength={64} />
329          </Form.Item>
330          <Form.Item
331            label={t("filament.fields.external_id")}
332            name={["external_id"]}
333            rules={[
334              {
335                required: false,
336              },
337            ]}
338          >
339            <Input maxLength={64} />
340          </Form.Item>
341          <Form.Item
342            label={t("filament.fields.comment")}
343            name={["comment"]}
344            rules={[
345              {
346                required: false,
347              },
348            ]}
349          >
350            <TextArea maxLength={1024} />
351          </Form.Item>
352          <Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
353          {extraFields.data?.map((field, index) => (
354            <ExtraFieldFormItem key={index} field={field} />
355          ))}
356        </Form>
357        {hasChanged && <Alert description={t("filament.form.filament_updated")} type="warning" showIcon />}
358      </Edit>
359    );
360  };
361  
362  export default FilamentEdit;