Extra fields now finished for filaments

This commit is contained in:
Donkie
2024-01-23 18:58:04 +01:00
parent 23818c1005
commit fb23024ed0
18 changed files with 699 additions and 139 deletions

View File

@@ -1,11 +1,20 @@
import React from "react";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select, InputNumber, ColorPicker, Button } from "antd";
import TextArea from "antd/es/input/TextArea";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { IVendor } from "../vendors/model";
import { IFilament } from "./model";
import { EntityType, FieldType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
type IFilamentParsedExtras = Omit<IFilament, "extra"> & { extra?: { [key: string]: unknown } };
interface CreateOrCloneProps {
mode: "create" | "clone";
@@ -13,10 +22,20 @@ interface CreateOrCloneProps {
export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.filament);
const { form, formProps, formLoading, onFinish, redirect } = useForm<IFilament>();
const { form, formProps, formLoading, onFinish, redirect } = useForm<
IFilament,
HttpError,
IFilamentParsedExtras,
IFilamentParsedExtras
>();
if (props.mode === "clone" && formProps.initialValues) {
if (!formProps.initialValues) {
formProps.initialValues = {};
}
if (props.mode === "clone") {
// Fix the vendor_id
if (formProps.initialValues.vendor) {
formProps.initialValues.vendor_id = formProps.initialValues.vendor.id;
@@ -24,7 +43,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
}
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
const values = await form.validateFields();
const values = StringifiedExtras(await form.validateFields());
await onFinish(values);
redirect(redirectTo, (values as IFilament).id);
};
@@ -34,6 +53,21 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
optionLabel: "name",
});
// Use useEffect to update the form's initialValues when the extra fields are loaded
// This is necessary because the form is rendered before the extra fields are loaded
React.useEffect(() => {
extraFields.data?.forEach((field) => {
if (formProps.initialValues && field.default_value) {
let parsedValue = JSON.parse(field.default_value as string);
if (field.field_type === FieldType.datetime) {
parsedValue = dayjs(parsedValue);
}
form.setFieldsValue({ extra: { [field.key]: parsedValue } });
}
});
}, [form, extraFields.data, formProps.initialValues]);
return (
<Create
title={props.mode === "create" ? t("filament.titles.create") : t("filament.titles.clone")}
@@ -229,6 +263,9 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
>
<TextArea maxLength={1024} />
</Form.Item>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />
))}
</Form>
</Create>
);

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Edit, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, DatePicker, Select, InputNumber, ColorPicker, message, Alert } from "antd";
import dayjs from "dayjs";
@@ -7,30 +7,62 @@ import TextArea from "antd/es/input/TextArea";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { IVendor } from "../vendors/model";
import { IFilament } from "./model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { ParsedExtras } from "../../components/extraFields";
// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
type IFilamentParsedExtras = Omit<IFilament, "extra"> & { extra?: { [key: string]: unknown } };
/*
The API returns the extra fields as JSON values, but we need to parse them into their real types
in order for Ant design's form to work properly. ParsedExtras does this for us.
We also need to stringify them again before sending them back to the API, which is done by overriding
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
*/
export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const [messageApi, contextHolder] = message.useMessage();
const [hasChanged, setHasChanged] = useState(false);
const extraFields = useGetFields(EntityType.filament);
const { formProps, saveButtonProps } = useForm<IFilament>({
const { formProps, saveButtonProps } = useForm<IFilament, HttpError, IFilamentParsedExtras, IFilamentParsedExtras>({
liveMode: "manual",
onLiveEvent() {
// Warn the user if the filament has been updated since the form was opened
messageApi.warning(t("filament.form.filament_updated"));
setHasChanged(true);
},
queryOptions: {
select(data) {
return {
data: ParsedExtras(data.data),
};
},
},
});
// Get vendor selection options
const { selectProps } = useSelect<IVendor>({
resource: "vendor",
optionLabel: "name",
});
// Add the vendor_id field to the form
if (formProps.initialValues) {
formProps.initialValues["vendor_id"] = formProps.initialValues["vendor"]?.id;
}
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: IFilamentParsedExtras) => {
if (allValues !== undefined && allValues !== null) {
allValues = StringifiedExtras(allValues);
}
originalOnFinish?.(allValues);
};
return (
<Edit saveButtonProps={saveButtonProps}>
{contextHolder}
@@ -246,6 +278,9 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
>
<TextArea maxLength={1024} />
</Form.Item>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />
))}
</Form>
{hasChanged && <Alert description={t("filament.form.filament_updated")} type="warning" showIcon />}
</Edit>

View File

@@ -15,6 +15,7 @@ import {
SortedColumn,
SpoolIconColumn,
ActionsColumn,
CustomFieldColumn,
} from "../../components/column";
import {
useSpoolmanArticleNumbers,
@@ -24,9 +25,13 @@ import {
} from "../../components/otherModels";
import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useNavigate } from "react-router-dom";
dayjs.extend(utc);
// Extra is a key-value dict
interface IFilamentCollapsed extends Omit<IFilament, "vendor"> {
"vendor.name": string | null;
}
@@ -71,6 +76,10 @@ const defaultColumns = allColumns.filter(
export const FilamentList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const invalidate = useInvalidate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.filament);
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
// Load initial state
const initialState = useInitialTableState(namespace);
@@ -145,6 +154,15 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("filament", record.id) },
];
const commonProps = {
t,
navigate,
actions,
dataSource,
tableState,
sorter: true,
};
return (
<List
headerButtons={({ defaultButtons }) => (
@@ -163,10 +181,20 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
<Dropdown
trigger={["click"]}
menu={{
items: allColumns.map((column_id) => ({
key: column_id,
label: t(translateColumnI18nKey(column_id)),
})),
items: allColumnsWithExtraFields.map((column_id) => {
if (column_id.indexOf("extra.") === 0) {
const extraField = extraFields.data?.find((field) => "extra." + field.key === column_id);
return {
key: column_id,
label: extraField?.name ?? column_id,
};
}
return {
key: column_id,
label: t(translateColumnI18nKey(column_id)),
};
}),
selectedKeys: showColumns,
selectable: true,
multiple: true,
@@ -195,129 +223,107 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
rowKey="id"
columns={removeUndefined([
SortedColumn({
...commonProps,
id: "id",
i18ncat: "filament",
actions,
dataSource,
tableState,
width: 70,
}),
FilteredQueryColumn({
...commonProps,
id: "vendor.name",
i18nkey: "filament.fields.vendor_name",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanVendors(),
}),
SpoolIconColumn({
...commonProps,
id: "name",
i18ncat: "filament",
color: (record: IFilamentCollapsed) => record.color_hex,
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanFilamentNames(),
}),
FilteredQueryColumn({
...commonProps,
id: "material",
i18ncat: "filament",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanMaterials(),
width: 110,
}),
SortedColumn({
...commonProps,
id: "price",
i18ncat: "filament",
actions,
dataSource,
tableState,
width: 80,
}),
NumberColumn({
...commonProps,
id: "density",
i18ncat: "filament",
unit: "g/cm³",
decimals: 2,
actions,
dataSource,
tableState,
maxDecimals: 2,
width: 100,
}),
NumberColumn({
...commonProps,
id: "diameter",
i18ncat: "filament",
unit: "mm",
decimals: 2,
actions,
dataSource,
tableState,
maxDecimals: 2,
width: 100,
}),
NumberColumn({
...commonProps,
id: "weight",
i18ncat: "filament",
unit: "g",
decimals: 1,
actions,
dataSource,
tableState,
maxDecimals: 1,
width: 100,
}),
NumberColumn({
...commonProps,
id: "spool_weight",
i18ncat: "filament",
unit: "g",
decimals: 1,
actions,
dataSource,
tableState,
maxDecimals: 1,
width: 100,
}),
FilteredQueryColumn({
...commonProps,
id: "article_number",
i18ncat: "filament",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanArticleNumbers(),
width: 130,
}),
NumberColumn({
...commonProps,
id: "settings_extruder_temp",
i18ncat: "filament",
unit: "°C",
decimals: 0,
actions,
dataSource,
tableState,
maxDecimals: 0,
width: 100,
}),
NumberColumn({
...commonProps,
id: "settings_bed_temp",
i18ncat: "filament",
unit: "°C",
decimals: 0,
actions,
dataSource,
tableState,
maxDecimals: 0,
width: 100,
}),
DateColumn({
...commonProps,
id: "registered",
i18ncat: "filament",
actions,
dataSource,
tableState,
}),
...(extraFields.data?.map((field) => {
return CustomFieldColumn({
...commonProps,
field,
});
}) ?? []),
RichColumn({
...commonProps,
id: "comment",
i18ncat: "filament",
actions,
dataSource,
tableState,
width: 150,
}),
ActionsColumn(actions),

View File

@@ -16,4 +16,5 @@ export interface IFilament {
settings_extruder_temp?: number;
settings_bed_temp?: number;
color_hex?: string;
extra: { [key: string]: string };
}

View File

@@ -8,6 +8,8 @@ import utc from "dayjs/plugin/utc";
import { IFilament } from "./model";
import { enrichText } from "../../utils/parsing";
import { useNavigate } from "react-router-dom";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldDisplay } from "../../components/extraFields";
dayjs.extend(utc);
const { Title } = Typography;
@@ -15,6 +17,7 @@ const { Title } = Typography;
export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.filament);
const { queryResult } = useShow<IFilament>({
liveMode: "auto",
});
@@ -74,8 +77,6 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
/>
<Title level={5}>{t("filament.fields.name")}</Title>
<TextField value={record?.name} />
{/* <Title level={5}>{t("filament.fields.id")}</Title>
{vendorIsLoading ? <>Loading...</> : <>{vendorData?.data?.id}</>} */}
<Title level={5}>{t("filament.fields.color_hex")}</Title>
<TextField value={record?.color_hex} />
<Title level={5}>{t("filament.fields.material")}</Title>
@@ -134,6 +135,9 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={record?.article_number} />
<Title level={5}>{t("filament.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
{extraFields?.data?.map((field, index) => (
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />
))}
</Show>
);
};