extraFields.tsx
1 import { DateField, TextField } from "@refinedev/antd"; 2 import { Checkbox, Form, Input, InputNumber, Select, Typography } from "antd"; 3 import { FormItemProps, Rule } from "antd/es/form"; 4 import dayjs from "dayjs"; 5 import utc from "dayjs/plugin/utc"; 6 import { enrichText } from "../utils/parsing"; 7 import { Field, FieldType } from "../utils/queryFields"; 8 import { DateTimePicker } from "./dateTimePicker"; 9 import { InputNumberRange } from "./inputNumberRange"; 10 import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField"; 11 12 dayjs.extend(utc); 13 14 const { Title } = Typography; 15 16 /** 17 * Title and value to display an extra field. Used for the show pages. 18 * @param props 19 * @returns 20 */ 21 export function ExtraFieldDisplay(props: { field: Field; value: string | undefined }) { 22 const { field, value } = props; 23 24 let item; 25 if (value !== undefined) { 26 const parsedValue = JSON.parse(value); 27 28 if (field.field_type === FieldType.integer) { 29 item = ( 30 <NumberFieldUnit 31 value={parsedValue ?? ""} 32 unit={field.unit ?? ""} 33 options={{ 34 maximumFractionDigits: 0, 35 minimumFractionDigits: 0, 36 }} 37 /> 38 ); 39 } else if (field.field_type === FieldType.float) { 40 item = ( 41 <NumberFieldUnit 42 value={parsedValue ?? ""} 43 unit={field.unit ?? ""} 44 options={{ 45 maximumFractionDigits: 3, 46 minimumFractionDigits: 0, 47 }} 48 /> 49 ); 50 } else if (field.field_type === FieldType.integer_range || field.field_type === FieldType.float_range) { 51 if (!Array.isArray(parsedValue) || parsedValue.length !== 2) { 52 return <TextField value={parsedValue} />; 53 } 54 item = ( 55 <NumberFieldUnitRange 56 value={parsedValue} 57 unit={field.unit ?? ""} 58 options={{ 59 maximumFractionDigits: field.field_type === FieldType.float_range ? 3 : 0, 60 minimumFractionDigits: 0, 61 }} 62 /> 63 ); 64 } else if (field.field_type === FieldType.text) { 65 item = <TextField value={enrichText(parsedValue)} />; 66 } else if (field.field_type === FieldType.datetime) { 67 item = ( 68 <DateField 69 value={dayjs.utc(parsedValue).local()} 70 title={dayjs.utc(parsedValue).local().format()} 71 format="YYYY-MM-DD HH:mm:ss" 72 /> 73 ); 74 } else if (field.field_type === FieldType.boolean) { 75 item = <TextField value={parsedValue ? "Yes" : "No"} />; 76 } else if (field.field_type === FieldType.choice && !field.multi_choice) { 77 item = <TextField value={parsedValue} />; 78 } else if (field.field_type === FieldType.choice && field.multi_choice) { 79 item = <TextField value={parsedValue.join(", ")} />; 80 } else { 81 throw new Error(`Unknown field type: ${field.field_type}`); 82 } 83 } else { 84 item = <></>; 85 } 86 87 return ( 88 <> 89 <Title level={5}>{field.name}</Title> 90 {item} 91 </> 92 ); 93 } 94 95 /** 96 * Form item for an extra field. Used for the edit pages. 97 * @param props 98 * @returns 99 */ 100 export function ExtraFieldFormItem(props: { field: Field; setDefaultValue?: boolean }) { 101 const { field } = props; 102 103 let inputNode; 104 const rules: Rule[] = [ 105 { 106 required: false, 107 }, 108 ]; 109 const formItemProps: FormItemProps = {}; 110 if (field.field_type === FieldType.integer) { 111 inputNode = <InputNumber addonAfter={field.unit} precision={0} />; 112 rules.push({ 113 type: "integer", 114 }); 115 } else if (field.field_type === FieldType.float) { 116 inputNode = <InputNumber addonAfter={field.unit} precision={3} />; 117 rules.push({ 118 type: "number", 119 }); 120 } else if (field.field_type === FieldType.integer_range) { 121 inputNode = <InputNumberRange unit={field.unit} precision={0} />; 122 } else if (field.field_type === FieldType.float_range) { 123 inputNode = <InputNumberRange unit={field.unit} precision={3} />; 124 } else if (field.field_type === FieldType.text) { 125 inputNode = <Input />; 126 rules.push({ 127 type: "string", 128 }); 129 } else if (field.field_type === FieldType.datetime) { 130 inputNode = <DateTimePicker />; 131 } else if (field.field_type === FieldType.boolean) { 132 inputNode = <Checkbox />; 133 formItemProps.valuePropName = "checked"; 134 rules.push({ 135 type: "boolean", 136 }); 137 } else if (field.field_type === FieldType.choice) { 138 inputNode = ( 139 <Select 140 mode={field.multi_choice ? "multiple" : undefined} 141 options={field.choices?.map((choice) => ({ label: choice, value: choice }))} 142 /> 143 ); 144 rules.push({ 145 type: field.multi_choice ? "array" : "string", 146 }); 147 } else { 148 throw new Error(`Unknown field type: ${field.field_type}`); 149 } 150 151 if (props.setDefaultValue) { 152 formItemProps.initialValue = field.default_value; 153 } 154 155 return ( 156 <Form.Item label={field.name} name={["extra", field.key]} rules={rules} {...formItemProps}> 157 {inputNode} 158 </Form.Item> 159 ); 160 } 161 162 /** 163 * Convert the string-based value extra key-values of an entity to their JSON-parsed values. 164 * @param obj 165 * @returns 166 */ 167 export function ParsedExtras<T extends { extra?: { [key: string]: string } }>( 168 obj: T, 169 ): Omit<T, "extra"> & { extra?: { [key: string]: unknown } } { 170 if (obj.extra) { 171 const newExtra: { [key: string]: unknown } = {}; 172 Object.entries(obj.extra).forEach(([key, value]) => { 173 try { 174 newExtra[key] = JSON.parse(value); 175 } catch { 176 newExtra[key] = value; 177 } 178 }); 179 return { 180 ...obj, 181 extra: newExtra, 182 }; 183 } else { 184 return obj; 185 } 186 } 187 188 /** 189 * Convert the JSON-parsed value extra key-values of an entity to their string values. 190 * @param obj 191 * @returns 192 */ 193 export function StringifiedExtras<T extends { extra?: { [key: string]: unknown } }>( 194 obj: T, 195 ): Omit<T, "extra"> & { extra?: { [key: string]: string } } { 196 if (obj.extra) { 197 const newExtra: { [key: string]: string } = {}; 198 Object.entries(obj.extra).forEach(([key, value]) => { 199 newExtra[key] = JSON.stringify(value); 200 }); 201 return { 202 ...obj, 203 extra: newExtra, 204 }; 205 } else { 206 return { 207 ...obj, 208 extra: undefined, 209 }; 210 } 211 }