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;