/ client / src / pages / filaments / create.tsx
create.tsx
  1  import { Create, useForm, useSelect } from "@refinedev/antd";
  2  import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
  3  import { Button, ColorPicker, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
  4  import TextArea from "antd/es/input/TextArea";
  5  import dayjs from "dayjs";
  6  import utc from "dayjs/plugin/utc";
  7  import { useEffect, useState } from "react";
  8  import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
  9  import { FilamentImportModal } from "../../components/filamentImportModal";
 10  import { MultiColorPicker } from "../../components/multiColorPicker";
 11  import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
 12  import { ExternalFilament } from "../../utils/queryExternalDB";
 13  import { EntityType, useGetFields } from "../../utils/queryFields";
 14  import { getCurrencySymbol, useCurrency } from "../../utils/settings";
 15  import { getOrCreateVendorFromExternal } from "../vendors/functions";
 16  import { IVendor } from "../vendors/model";
 17  import { IFilament, IFilamentParsedExtras } from "./model";
 18  
 19  dayjs.extend(utc);
 20  
 21  interface CreateOrCloneProps {
 22    mode: "create" | "clone";
 23  }
 24  
 25  type IFilamentRequest = Omit<IFilamentParsedExtras, "id" | "registered"> & {
 26    vendor_id: number;
 27  };
 28  
 29  export const FilamentCreate = (props: IResourceComponentsProps & CreateOrCloneProps) => {
 30    const t = useTranslate();
 31    const extraFields = useGetFields(EntityType.filament);
 32    const currency = useCurrency();
 33    const [isImportExtOpen, setIsImportExtOpen] = useState(false);
 34    const invalidate = useInvalidate();
 35    const [colorType, setColorType] = useState<"single" | "multi">("single");
 36  
 37    const { form, formProps, formLoading, onFinish, redirect } = useForm<
 38      IFilament,
 39      HttpError,
 40      IFilamentRequest,
 41      IFilamentParsedExtras
 42    >();
 43  
 44    if (!formProps.initialValues) {
 45      formProps.initialValues = {};
 46    }
 47  
 48    if (props.mode === "clone") {
 49      // Fix the vendor_id
 50      if (formProps.initialValues.vendor) {
 51        formProps.initialValues.vendor_id = formProps.initialValues.vendor.id;
 52      }
 53  
 54      // Parse the extra fields from string values into real types
 55      formProps.initialValues = ParsedExtras(formProps.initialValues);
 56    }
 57  
 58    const handleSubmit = async (redirectTo: "list" | "create") => {
 59      const values = StringifiedExtras(await form.validateFields());
 60      await onFinish(values);
 61      redirect(redirectTo);
 62    };
 63  
 64    const { selectProps: vendorSelect } = useSelect<IVendor>({
 65      resource: "vendor",
 66      optionLabel: "name",
 67      pagination: { mode: "off" },
 68    });
 69  
 70    const importFilament = async (filament: ExternalFilament) => {
 71      const vendor = await getOrCreateVendorFromExternal(filament.manufacturer);
 72      await invalidate({
 73        resource: "vendor",
 74        invalidates: ["list", "detail"],
 75      });
 76  
 77      setColorType(filament.color_hexes ? "multi" : "single");
 78  
 79      form.setFieldsValue({
 80        name: filament.name,
 81        vendor_id: vendor.id,
 82        material: filament.material,
 83        density: filament.density,
 84        diameter: filament.diameter,
 85        weight: filament.weight,
 86        spool_weight: filament.spool_weight || undefined,
 87        color_hex: filament.color_hex,
 88        multi_color_hexes: filament.color_hexes?.join(",") || undefined,
 89        multi_color_direction: filament.multi_color_direction,
 90        settings_extruder_temp: filament.extruder_temp || undefined,
 91        settings_bed_temp: filament.bed_temp || undefined,
 92      });
 93    };
 94  
 95    // Use useEffect to update the form's initialValues when the extra fields are loaded
 96    // This is necessary because the form is rendered before the extra fields are loaded
 97    useEffect(() => {
 98      extraFields.data?.forEach((field) => {
 99        if (formProps.initialValues && field.default_value) {
100          const parsedValue = JSON.parse(field.default_value as string);
101          form.setFieldsValue({ extra: { [field.key]: parsedValue } });
102        }
103      });
104    }, [form, extraFields.data, formProps.initialValues]);
105  
106    return (
107      <Create
108        title={props.mode === "create" ? t("filament.titles.create") : t("filament.titles.clone")}
109        isLoading={formLoading}
110        headerButtons={() => (
111          <>
112            <Button type="primary" onClick={() => setIsImportExtOpen(true)}>
113              {t("filament.form.import_external")}
114            </Button>
115          </>
116        )}
117        footerButtons={() => (
118          <>
119            <Button type="primary" onClick={() => handleSubmit("list")}>
120              {t("buttons.save")}
121            </Button>
122            <Button type="primary" onClick={() => handleSubmit("create")}>
123              {t("buttons.saveAndAdd")}
124            </Button>
125          </>
126        )}
127      >
128        <FilamentImportModal
129          isOpen={isImportExtOpen}
130          onImport={(value) => {
131            setIsImportExtOpen(false);
132            importFilament(value);
133          }}
134          onClose={() => setIsImportExtOpen(false)}
135        />
136        <Form {...formProps} layout="vertical">
137          <Form.Item
138            label={t("filament.fields.name")}
139            help={t("filament.fields_help.name")}
140            name={["name"]}
141            rules={[
142              {
143                required: false,
144              },
145            ]}
146          >
147            <Input maxLength={64} />
148          </Form.Item>
149          <Form.Item
150            label={t("filament.fields.vendor")}
151            name={["vendor_id"]}
152            rules={[
153              {
154                required: false,
155              },
156            ]}
157          >
158            <Select
159              {...vendorSelect}
160              allowClear
161              filterSort={(a, b) => {
162                return a?.label && b?.label
163                  ? (a.label as string).localeCompare(b.label as string, undefined, { sensitivity: "base" })
164                  : 0;
165              }}
166              filterOption={(input, option) =>
167                typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
168              }
169            />
170          </Form.Item>
171          <Form.Item label={t("filament.fields.color_hex")}>
172            <Radio.Group
173              onChange={(value) => {
174                setColorType(value.target.value);
175              }}
176              defaultValue={colorType}
177              value={colorType}
178            >
179              <Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
180              <Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
181            </Radio.Group>
182          </Form.Item>
183          {colorType == "single" && (
184            <Form.Item
185              name={"color_hex"}
186              rules={[
187                {
188                  required: false,
189                },
190              ]}
191              getValueFromEvent={(e) => {
192                return e?.toHex();
193              }}
194            >
195              <ColorPicker format="hex" />
196            </Form.Item>
197          )}
198          {colorType == "multi" && (
199            <Form.Item
200              name={"multi_color_direction"}
201              help={t("filament.fields_help.multi_color_direction")}
202              rules={[
203                {
204                  required: true,
205                },
206              ]}
207              initialValue={"coaxial"}
208            >
209              <Radio.Group>
210                <Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
211                <Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
212              </Radio.Group>
213            </Form.Item>
214          )}
215          {colorType == "multi" && (
216            <Form.Item
217              name={"multi_color_hexes"}
218              rules={[
219                {
220                  required: false,
221                },
222              ]}
223            >
224              <MultiColorPicker min={2} max={14} />
225            </Form.Item>
226          )}
227          <Form.Item
228            label={t("filament.fields.material")}
229            help={t("filament.fields_help.material")}
230            name={["material"]}
231            rules={[
232              {
233                required: false,
234              },
235            ]}
236          >
237            <Input maxLength={64} />
238          </Form.Item>
239          <Form.Item
240            label={t("filament.fields.price")}
241            help={t("filament.fields_help.price")}
242            name={["price"]}
243            rules={[
244              {
245                required: false,
246                type: "number",
247                min: 0,
248              },
249            ]}
250          >
251            <InputNumber
252              addonAfter={getCurrencySymbol(undefined, currency)}
253              precision={2}
254              formatter={formatNumberOnUserInput}
255              parser={numberParserAllowEmpty}
256            />
257          </Form.Item>
258          <Form.Item
259            label={t("filament.fields.density")}
260            name={["density"]}
261            rules={[
262              {
263                required: true,
264                type: "number",
265                min: 0,
266                max: 100,
267              },
268            ]}
269          >
270            <InputNumber addonAfter="g/cm³" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
271          </Form.Item>
272          <Form.Item
273            label={t("filament.fields.diameter")}
274            name={["diameter"]}
275            rules={[
276              {
277                required: true,
278                type: "number",
279                min: 0,
280                max: 10,
281              },
282            ]}
283          >
284            <InputNumber addonAfter="mm" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
285          </Form.Item>
286          <Form.Item
287            label={t("filament.fields.weight")}
288            help={t("filament.fields_help.weight")}
289            name={["weight"]}
290            rules={[
291              {
292                required: false,
293                type: "number",
294                min: 0,
295              },
296            ]}
297          >
298            <InputNumber addonAfter="g" precision={1} />
299          </Form.Item>
300          <Form.Item
301            label={t("filament.fields.spool_weight")}
302            help={t("filament.fields_help.spool_weight")}
303            name={["spool_weight"]}
304            rules={[
305              {
306                required: false,
307                type: "number",
308                min: 0,
309              },
310            ]}
311          >
312            <InputNumber addonAfter="g" precision={1} />
313          </Form.Item>
314          <Form.Item
315            label={t("filament.fields.settings_extruder_temp")}
316            name={["settings_extruder_temp"]}
317            rules={[
318              {
319                required: false,
320                type: "number",
321                min: 0,
322              },
323            ]}
324          >
325            <InputNumber addonAfter="°C" precision={0} />
326          </Form.Item>
327          <Form.Item
328            label={t("filament.fields.settings_bed_temp")}
329            name={["settings_bed_temp"]}
330            rules={[
331              {
332                required: false,
333                type: "number",
334                min: 0,
335              },
336            ]}
337          >
338            <InputNumber addonAfter="°C" precision={0} />
339          </Form.Item>
340          <Form.Item
341            label={t("filament.fields.article_number")}
342            help={t("filament.fields_help.article_number")}
343            name={["article_number"]}
344            rules={[
345              {
346                required: false,
347              },
348            ]}
349          >
350            <Input maxLength={64} />
351          </Form.Item>
352          <Form.Item
353            label={t("filament.fields.comment")}
354            name={["comment"]}
355            rules={[
356              {
357                required: false,
358              },
359            ]}
360          >
361            <TextArea maxLength={1024} />
362          </Form.Item>
363          <Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
364          {extraFields.data?.map((field, index) => (
365            <ExtraFieldFormItem key={index} field={field} />
366          ))}
367        </Form>
368      </Create>
369    );
370  };
371  
372  export default FilamentCreate;