Extra fields now finished for filaments
This commit is contained in:
@@ -3,7 +3,7 @@ import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
|
||||
import { getFiltersForField, typeFilters } from "../utils/filtering";
|
||||
import { TableState } from "../utils/saveload";
|
||||
import { getSortOrderForField, typeSorters } from "../utils/sorting";
|
||||
import { NumberFieldUnit } from "./numberField";
|
||||
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { DateField, TextField } from "@refinedev/antd";
|
||||
@@ -12,7 +12,8 @@ import SpoolIcon from "../icon_spool.svg?react";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { enrichText } from "../utils/parsing";
|
||||
import { UseQueryResult } from "@tanstack/react-query";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Field, FieldType } from "../utils/queryFields";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -39,14 +40,19 @@ export interface Action {
|
||||
}
|
||||
|
||||
interface BaseColumnProps<Obj extends Entity> {
|
||||
id: keyof Obj & string;
|
||||
id: string | string[];
|
||||
dataId?: keyof Obj & string;
|
||||
i18ncat?: string;
|
||||
i18nkey?: string;
|
||||
title?: string;
|
||||
sorter?: boolean;
|
||||
t: (key: string) => string;
|
||||
navigate: (link: string) => void;
|
||||
dataSource: Obj[];
|
||||
tableState: TableState;
|
||||
width?: number;
|
||||
actions?: (record: Obj) => Action[];
|
||||
transform?: (value: unknown) => unknown;
|
||||
}
|
||||
|
||||
interface FilteredColumnProps {
|
||||
@@ -70,24 +76,32 @@ interface CustomColumnProps<Obj> {
|
||||
function Column<Obj extends Entity>(
|
||||
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>
|
||||
): ColumnType<Obj> | undefined {
|
||||
const t = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const t = props.t;
|
||||
const navigate = props.navigate;
|
||||
|
||||
// Hide if not in showColumns
|
||||
if (props.tableState.showColumns && !props.tableState.showColumns.includes(props.id)) {
|
||||
const id = Array.isArray(props.id) ? props.id.join(".") : props.id;
|
||||
if (props.tableState.showColumns && !props.tableState.showColumns.includes(id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const columnProps: ColumnType<Obj> = {
|
||||
dataIndex: props.id,
|
||||
title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
|
||||
sorter: true,
|
||||
sortOrder: getSortOrderForField(typeSorters<Obj>(props.tableState.sorters), props.dataId ?? props.id),
|
||||
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
|
||||
filterMultiple: props.allowMultipleFilters ?? true,
|
||||
width: props.width ?? undefined,
|
||||
onCell: props.onCell ?? undefined,
|
||||
};
|
||||
|
||||
// Sorting
|
||||
if (props.sorter) {
|
||||
columnProps.sorter = true;
|
||||
columnProps.sortOrder = getSortOrderForField(
|
||||
typeSorters<Obj>(props.tableState.sorters),
|
||||
props.dataId ?? (props.id as keyof Obj)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter
|
||||
if (props.filters && props.filteredValue) {
|
||||
columnProps.filters = props.filters;
|
||||
@@ -106,7 +120,12 @@ function Column<Obj extends Entity>(
|
||||
}
|
||||
|
||||
// Render
|
||||
const render = props.render ?? ((value) => <>{value}</>);
|
||||
const render =
|
||||
props.render ??
|
||||
((rawValue) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
return <>{value}</>;
|
||||
});
|
||||
columnProps.render = (value, record, index) => {
|
||||
if (!props.actions) {
|
||||
return render(value, record, index);
|
||||
@@ -144,13 +163,19 @@ function Column<Obj extends Entity>(
|
||||
}
|
||||
|
||||
export function SortedColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column(props);
|
||||
}
|
||||
|
||||
export function RichColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value: string | undefined) => {
|
||||
sorter: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function RichColumn<Obj extends Entity>(
|
||||
props: Omit<BaseColumnProps<Obj>, "transform"> & { transform?: (value: unknown) => string }
|
||||
) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (rawValue: string | undefined) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
return enrichText(value);
|
||||
},
|
||||
});
|
||||
@@ -182,7 +207,7 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
|
||||
});
|
||||
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? props.id);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj));
|
||||
|
||||
const onFilterDropdownOpen = () => {
|
||||
query.refetch();
|
||||
@@ -193,14 +218,16 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
|
||||
|
||||
interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
unit: string;
|
||||
decimals?: number;
|
||||
maxDecimals?: number;
|
||||
minDecimals?: number;
|
||||
defaultText?: string;
|
||||
}
|
||||
|
||||
export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value) => {
|
||||
render: (rawValue) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
if (value === null || value === undefined) {
|
||||
return <TextField value={props.defaultText ?? ""} />;
|
||||
}
|
||||
@@ -209,8 +236,8 @@ export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>)
|
||||
value={value}
|
||||
unit={props.unit}
|
||||
options={{
|
||||
maximumFractionDigits: props.decimals ?? 0,
|
||||
minimumFractionDigits: props.decimals ?? 0,
|
||||
maximumFractionDigits: props.maxDecimals ?? 0,
|
||||
minimumFractionDigits: props.minDecimals ?? props.maxDecimals ?? 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -221,7 +248,8 @@ export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>)
|
||||
export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value) => {
|
||||
render: (rawValue) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
return (
|
||||
<DateField
|
||||
hidden={!value}
|
||||
@@ -291,7 +319,7 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
|
||||
});
|
||||
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? props.id);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj));
|
||||
|
||||
const onFilterDropdownOpen = () => {
|
||||
query.refetch();
|
||||
@@ -312,7 +340,8 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
|
||||
},
|
||||
};
|
||||
},
|
||||
render: (value, record: Obj) => {
|
||||
render: (rawValue, record: Obj) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
const colorStr = props.color(record);
|
||||
return (
|
||||
<Row wrap={false} justify="space-around" align="middle">
|
||||
@@ -334,3 +363,121 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function NumberRangeColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (rawValue) => {
|
||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||
if (value === null || value === undefined) {
|
||||
return <TextField value={props.defaultText ?? ""} />;
|
||||
}
|
||||
if (!Array.isArray(value) || value.length !== 2) {
|
||||
return <TextField value={props.defaultText ?? ""} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NumberFieldUnitRange
|
||||
value={value}
|
||||
unit={props.unit}
|
||||
options={{
|
||||
maximumFractionDigits: props.maxDecimals ?? 0,
|
||||
minimumFractionDigits: props.minDecimals ?? props.maxDecimals ?? 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function CustomFieldColumn<Obj extends Entity>(props: Omit<BaseColumnProps<Obj>, "id"> & { field: Field }) {
|
||||
const field = props.field;
|
||||
const commonProps = {
|
||||
...props,
|
||||
id: ["extra", field.key],
|
||||
title: field.name,
|
||||
sorter: false,
|
||||
transform: (value: unknown) => {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(value as string);
|
||||
},
|
||||
};
|
||||
|
||||
if (field.field_type === FieldType.integer) {
|
||||
return NumberColumn({
|
||||
...commonProps,
|
||||
unit: field.unit ?? "",
|
||||
maxDecimals: 0,
|
||||
});
|
||||
} else if (field.field_type === FieldType.float) {
|
||||
return NumberColumn({
|
||||
...commonProps,
|
||||
unit: field.unit ?? "",
|
||||
minDecimals: 0,
|
||||
maxDecimals: 3,
|
||||
});
|
||||
} else if (field.field_type === FieldType.integer_range) {
|
||||
return NumberRangeColumn({
|
||||
...commonProps,
|
||||
unit: field.unit ?? "",
|
||||
maxDecimals: 0,
|
||||
});
|
||||
} else if (field.field_type === FieldType.float_range) {
|
||||
return NumberRangeColumn({
|
||||
...commonProps,
|
||||
unit: field.unit ?? "",
|
||||
minDecimals: 0,
|
||||
maxDecimals: 3,
|
||||
});
|
||||
} else if (field.field_type === FieldType.text) {
|
||||
return RichColumn({
|
||||
...commonProps,
|
||||
});
|
||||
} else if (field.field_type === FieldType.datetime) {
|
||||
return DateColumn({
|
||||
...commonProps,
|
||||
});
|
||||
} else if (field.field_type === FieldType.boolean) {
|
||||
return Column({
|
||||
...commonProps,
|
||||
render: (rawValue) => {
|
||||
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
|
||||
let text;
|
||||
if (value === undefined || value === null) {
|
||||
text = "";
|
||||
} else if (value) {
|
||||
text = props.t("yes");
|
||||
} else {
|
||||
text = props.t("no");
|
||||
}
|
||||
return <TextField value={text} />;
|
||||
},
|
||||
});
|
||||
} else if (field.field_type === FieldType.choice && !field.multi_choice) {
|
||||
return Column({
|
||||
...commonProps,
|
||||
render: (rawValue) => {
|
||||
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
|
||||
return <TextField value={value} />;
|
||||
},
|
||||
});
|
||||
} else if (field.field_type === FieldType.choice && field.multi_choice) {
|
||||
return Column({
|
||||
...commonProps,
|
||||
render: (rawValue) => {
|
||||
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
|
||||
return <TextField value={(value as string[] | undefined)?.join(", ")} />;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return Column({
|
||||
...commonProps,
|
||||
render: (rawValue) => {
|
||||
const value = commonProps.transform ? commonProps.transform(rawValue) : rawValue;
|
||||
return <TextField value={value} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
33
client/src/components/dateTimePicker.tsx
Normal file
33
client/src/components/dateTimePicker.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DatePicker } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(advancedFormat);
|
||||
|
||||
// Localized date time format with timezone
|
||||
const dateTimeFormat = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
export function DateTimePicker<T extends string | dayjs.Dayjs>(props: { value?: T; onChange?: (value?: T) => void }) {
|
||||
return (
|
||||
<DatePicker
|
||||
showTime={{ use12Hours: false }}
|
||||
format={dateTimeFormat}
|
||||
value={props.value ? dayjs(props.value) : undefined}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
if (typeof props.value === "string") {
|
||||
props.onChange?.(value.toISOString() as T);
|
||||
} else {
|
||||
props.onChange?.(value as T);
|
||||
}
|
||||
} else {
|
||||
props.onChange?.(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
211
client/src/components/extraFields.tsx
Normal file
211
client/src/components/extraFields.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { DateField, TextField } from "@refinedev/antd";
|
||||
import { Field, FieldType } from "../utils/queryFields";
|
||||
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
|
||||
import { enrichText } from "../utils/parsing";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { Checkbox, Form, Input, InputNumber, Select, Typography } from "antd";
|
||||
import { FormItemProps, Rule } from "antd/es/form";
|
||||
import { DateTimePicker } from "./dateTimePicker";
|
||||
import { InputNumberRange } from "./inputNumberRange";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
/**
|
||||
* Title and value to display an extra field. Used for the show pages.
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function ExtraFieldDisplay(props: { field: Field; value: string | undefined }) {
|
||||
const { field, value } = props;
|
||||
|
||||
let item;
|
||||
if (value !== undefined) {
|
||||
const parsedValue = JSON.parse(value);
|
||||
|
||||
if (field.field_type === FieldType.integer) {
|
||||
item = (
|
||||
<NumberFieldUnit
|
||||
value={parsedValue ?? ""}
|
||||
unit={field.unit ?? ""}
|
||||
options={{
|
||||
maximumFractionDigits: 0,
|
||||
minimumFractionDigits: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (field.field_type === FieldType.float) {
|
||||
item = (
|
||||
<NumberFieldUnit
|
||||
value={parsedValue ?? ""}
|
||||
unit={field.unit ?? ""}
|
||||
options={{
|
||||
maximumFractionDigits: 3,
|
||||
minimumFractionDigits: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (field.field_type === FieldType.integer_range || field.field_type === FieldType.float_range) {
|
||||
if (!Array.isArray(parsedValue) || parsedValue.length !== 2) {
|
||||
return <TextField value={parsedValue} />;
|
||||
}
|
||||
item = (
|
||||
<NumberFieldUnitRange
|
||||
value={parsedValue}
|
||||
unit={field.unit ?? ""}
|
||||
options={{
|
||||
maximumFractionDigits: field.field_type === FieldType.float_range ? 3 : 0,
|
||||
minimumFractionDigits: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (field.field_type === FieldType.text) {
|
||||
item = <TextField value={enrichText(parsedValue)} />;
|
||||
} else if (field.field_type === FieldType.datetime) {
|
||||
item = (
|
||||
<DateField
|
||||
value={dayjs.utc(parsedValue).local()}
|
||||
title={dayjs.utc(parsedValue).local().format()}
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
);
|
||||
} else if (field.field_type === FieldType.boolean) {
|
||||
item = <TextField value={parsedValue ? "Yes" : "No"} />;
|
||||
} else if (field.field_type === FieldType.choice && !field.multi_choice) {
|
||||
item = <TextField value={parsedValue} />;
|
||||
} else if (field.field_type === FieldType.choice && field.multi_choice) {
|
||||
item = <TextField value={parsedValue.join(", ")} />;
|
||||
} else {
|
||||
throw new Error(`Unknown field type: ${field.field_type}`);
|
||||
}
|
||||
} else {
|
||||
item = <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={5}>{field.name}</Title>
|
||||
{item}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form item for an extra field. Used for the edit pages.
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function ExtraFieldFormItem(props: { field: Field; setDefaultValue?: boolean }) {
|
||||
const { field } = props;
|
||||
|
||||
let inputNode;
|
||||
const rules: Rule[] = [
|
||||
{
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
const formItemProps: FormItemProps = {};
|
||||
if (field.field_type === FieldType.integer) {
|
||||
inputNode = <InputNumber addonAfter={field.unit} precision={0} />;
|
||||
rules.push({
|
||||
type: "integer",
|
||||
});
|
||||
} else if (field.field_type === FieldType.float) {
|
||||
inputNode = <InputNumber addonAfter={field.unit} precision={3} />;
|
||||
rules.push({
|
||||
type: "number",
|
||||
});
|
||||
} else if (field.field_type === FieldType.integer_range) {
|
||||
inputNode = <InputNumberRange unit={field.unit} precision={0} />;
|
||||
} else if (field.field_type === FieldType.float_range) {
|
||||
inputNode = <InputNumberRange unit={field.unit} precision={3} />;
|
||||
} else if (field.field_type === FieldType.text) {
|
||||
inputNode = <Input />;
|
||||
rules.push({
|
||||
type: "string",
|
||||
});
|
||||
} else if (field.field_type === FieldType.datetime) {
|
||||
inputNode = <DateTimePicker />;
|
||||
} else if (field.field_type === FieldType.boolean) {
|
||||
inputNode = <Checkbox />;
|
||||
formItemProps.valuePropName = "checked";
|
||||
rules.push({
|
||||
type: "boolean",
|
||||
});
|
||||
} else if (field.field_type === FieldType.choice) {
|
||||
inputNode = (
|
||||
<Select
|
||||
mode={field.multi_choice ? "multiple" : undefined}
|
||||
options={field.choices?.map((choice) => ({ label: choice, value: choice }))}
|
||||
/>
|
||||
);
|
||||
rules.push({
|
||||
type: field.multi_choice ? "array" : "string",
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown field type: ${field.field_type}`);
|
||||
}
|
||||
|
||||
if (props.setDefaultValue) {
|
||||
formItemProps.initialValue = field.default_value;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item label={field.name} name={["extra", field.key]} rules={rules} {...formItemProps}>
|
||||
{inputNode}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the string-based value extra key-values of an entity to their JSON-parsed values.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export function ParsedExtras<T extends { extra?: { [key: string]: string } }>(
|
||||
obj: T
|
||||
): Omit<T, "extra"> & { extra?: { [key: string]: unknown } } {
|
||||
if (obj.extra) {
|
||||
const newExtra: { [key: string]: unknown } = {};
|
||||
Object.entries(obj.extra).forEach(([key, value]) => {
|
||||
try {
|
||||
newExtra[key] = JSON.parse(value);
|
||||
} catch (e) {
|
||||
newExtra[key] = value;
|
||||
}
|
||||
});
|
||||
return {
|
||||
...obj,
|
||||
extra: newExtra,
|
||||
};
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the JSON-parsed value extra key-values of an entity to their string values.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export function StringifiedExtras<T extends { extra?: { [key: string]: unknown } }>(
|
||||
obj: T
|
||||
): Omit<T, "extra"> & { extra?: { [key: string]: string } } {
|
||||
if (obj.extra) {
|
||||
const newExtra: { [key: string]: string } = {};
|
||||
Object.entries(obj.extra).forEach(([key, value]) => {
|
||||
newExtra[key] = JSON.stringify(value);
|
||||
});
|
||||
return {
|
||||
...obj,
|
||||
extra: newExtra,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...obj,
|
||||
extra: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
57
client/src/components/inputNumberRange.tsx
Normal file
57
client/src/components/inputNumberRange.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { InputNumber } from "antd";
|
||||
|
||||
function parseValue(value?: (number | null)[]): [number | null, number | null] {
|
||||
if (value === undefined) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.length !== 2) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
const [min, max] = value;
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
/**
|
||||
* An Ant Design compatible form input for a number range.
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function InputNumberRange(props: {
|
||||
/** Decimal precision */ precision?: number;
|
||||
/** Unit to display */ unit?: string;
|
||||
/** Value state */ value?: (number | null)[] | undefined;
|
||||
/** Called when input changes. Used to update value state. */ onChange?: (value: (number | null)[]) => void;
|
||||
}) {
|
||||
const [min, max] = parseValue(props.value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputNumber
|
||||
value={min}
|
||||
precision={props.precision ?? 0}
|
||||
addonAfter={props.unit}
|
||||
style={{ maxWidth: 110 }}
|
||||
onChange={(value) => {
|
||||
if (props.onChange) {
|
||||
props.onChange([value, max]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{" \u2013 "}
|
||||
<InputNumber
|
||||
value={max}
|
||||
precision={props.precision ?? 0}
|
||||
addonAfter={props.unit}
|
||||
style={{ maxWidth: 110 }}
|
||||
onChange={(value) => {
|
||||
if (props.onChange) {
|
||||
props.onChange([min, value]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,3 +26,36 @@ export const NumberFieldUnit: React.FC<Props> = ({ value, locale, options, ...re
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Like a {@link NumberFieldUnit} but for a range of numbers.
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function NumberFieldUnitRange(props: {
|
||||
value: (number | null)[] | undefined;
|
||||
unit?: string;
|
||||
options?: Intl.NumberFormatOptions;
|
||||
}) {
|
||||
const { value, unit, options } = props;
|
||||
|
||||
if (value === undefined) {
|
||||
console.warn("NumberFieldUnitRange received undefined value");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.length !== 2) {
|
||||
console.warn("NumberFieldUnitRange received invalid value", value);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const [min, max] = value;
|
||||
|
||||
return (
|
||||
<>
|
||||
{min === null ? <></> : <NumberFieldUnit value={min} unit={unit ?? ""} options={options} />}
|
||||
{" \u2013 "}
|
||||
{max === null ? <></> : <NumberFieldUnit value={max} unit={unit ?? ""} options={options} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user