/ client / src / components / extraFields.tsx
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  }