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;