Implemented extra fields for spools and vendors as well

This commit is contained in:
Donkie
2024-01-23 19:27:34 +01:00
parent fb23024ed0
commit 40d3a3184f
19 changed files with 287 additions and 125 deletions

View File

@@ -1,15 +1,20 @@
import React, { useState } 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, DatePicker, Select, InputNumber, Radio, Divider, Button } from "antd";
import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Button, Typography } from "antd";
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { IFilament } from "../filaments/model";
import { ISpool } from "./model";
import { ISpool, ISpoolParsedExtras } from "./model";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { useSpoolmanLocations } from "../../components/otherModels";
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
import "../../utils/overrides.css";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
interface CreateOrCloneProps {
mode: "create" | "clone";
@@ -17,13 +22,22 @@ interface CreateOrCloneProps {
export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.spool);
const { form, formProps, formLoading, onFinish, redirect } = useForm<ISpool>({
const { form, formProps, formLoading, onFinish, redirect } = useForm<
ISpool,
HttpError,
ISpoolParsedExtras,
ISpoolParsedExtras
>({
redirect: false,
warnWhenUnsavedChanges: false,
});
if (!formProps.initialValues) {
formProps.initialValues = {};
}
if (props.mode === "clone" && formProps.initialValues) {
if (props.mode === "clone") {
// Clear out the values that we don't want to clone
formProps.initialValues.first_used = null;
formProps.initialValues.last_used = null;
@@ -34,7 +48,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
}
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
const values = await form.validateFields();
const values = StringifiedExtras(await form.validateFields());
if (quantity > 1) {
const submit = Array(quantity).fill(values);
// queue multiple creates this way for now Refine doesn't seem to map Arrays to createMany or multiple creates like it says it does
@@ -49,6 +63,17 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
resource: "filament",
});
// 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) {
const parsedValue = JSON.parse(field.default_value as string);
form.setFieldsValue({ extra: { [field.key]: parsedValue } });
}
});
}, [form, extraFields.data, formProps.initialValues]);
const filamentOptions = queryResult.data?.data.map((item) => {
let vendorPrefix = "";
if (item.vendor) {
@@ -213,7 +238,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
},
]}
>
<InputNumber precision={2} formatter={numberFormatter} parser={numberParser} />
<InputNumber precision={2} formatter={numberFormatter} parser={numberParser} />
</Form.Item>
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
<InputNumber value={usedWeight} />
@@ -336,6 +361,10 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
>
<TextArea maxLength={1024} />
</Form.Item>
<Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />
))}
</Form>
</Create>
);

View File

@@ -1,33 +1,61 @@
import React, { useEffect, 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, Radio, Divider, Alert } from "antd";
import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Alert, Typography } from "antd";
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { IFilament } from "../filaments/model";
import { ISpool } from "./model";
import { ISpool, ISpoolParsedExtras } from "./model";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { useSpoolmanLocations } from "../../components/otherModels";
import { message } from "antd/lib";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import { ParsedExtras } from "../../components/extraFields";
/*
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 SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const [messageApi, contextHolder] = message.useMessage();
const [hasChanged, setHasChanged] = useState(false);
const extraFields = useGetFields(EntityType.spool);
const { form, formProps, saveButtonProps } = useForm<ISpool>({
const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpoolParsedExtras, ISpoolParsedExtras>({
liveMode: "manual",
onLiveEvent() {
// Warn the user if the spool has been updated since the form was opened
messageApi.warning(t("spool.form.spool_updated"));
setHasChanged(true);
},
queryOptions: {
select(data) {
return {
data: ParsedExtras(data.data),
};
},
},
});
// Get filament selection options
const { queryResult } = useSelect<IFilament>({
resource: "filament",
});
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: ISpoolParsedExtras) => {
if (allValues !== undefined && allValues !== null) {
allValues = StringifiedExtras(allValues);
}
originalOnFinish?.(allValues);
};
const filamentOptions = queryResult.data?.data.map((item) => {
let vendorPrefix = "";
if (item.vendor) {
@@ -306,6 +334,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
>
<TextArea maxLength={1024} />
</Form.Item>
<Typography.Title level={5}>{t("settings.extra_fields.tab")}</Typography.Title>
{extraFields.data?.map((field, index) => (
<ExtraFieldFormItem key={index} field={field} />
))}
</Form>
{hasChanged && <Alert description={t("spool.form.spool_updated")} type="warning" showIcon />}
</Edit>

View File

@@ -23,6 +23,7 @@ import {
SpoolIconColumn,
Action,
ActionsColumn,
CustomFieldColumn,
} from "../../components/column";
import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog";
@@ -34,6 +35,8 @@ 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);
@@ -95,6 +98,10 @@ const defaultColumns = allColumns.filter(
export const SpoolList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const invalidate = useInvalidate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.spool);
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
// Load initial state
const initialState = useInitialTableState(namespace);
@@ -217,6 +224,15 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
return actions;
};
const commonProps = {
t,
navigate,
actions,
dataSource,
tableState,
sorter: true,
};
return (
<List
headerButtons={({ defaultButtons }) => (
@@ -245,10 +261,20 @@ export const SpoolList: 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,
@@ -290,140 +316,106 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
}}
columns={removeUndefined([
SortedColumn({
...commonProps,
id: "id",
i18ncat: "spool",
actions,
dataSource,
tableState,
width: 70,
}),
SpoolIconColumn({
...commonProps,
id: "combined_name",
i18nkey: "spool.fields.filament_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
actions,
dataSource,
tableState,
dataId: "filament.id",
filterValueQuery: useSpoolmanFilamentFilter(),
sorter: true,
}),
FilteredQueryColumn({
...commonProps,
id: "filament.material",
i18nkey: "spool.fields.material",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanMaterials(),
width: 120,
sorter: true,
}),
SortedColumn({
...commonProps,
id: "price",
i18ncat: "spool",
actions,
dataSource,
tableState,
width: 80,
}),
NumberColumn({
...commonProps,
id: "used_weight",
i18ncat: "spool",
unit: "g",
maxDecimals: 1,
actions,
dataSource,
tableState,
width: 110,
sorter: true,
}),
NumberColumn({
...commonProps,
id: "remaining_weight",
i18ncat: "spool",
unit: "g",
maxDecimals: 1,
defaultText: t("unknown"),
actions,
dataSource,
tableState,
width: 110,
sorter: true,
}),
NumberColumn({
...commonProps,
id: "used_length",
i18ncat: "spool",
unit: "mm",
maxDecimals: 1,
actions,
dataSource,
tableState,
width: 120,
sorter: true,
}),
NumberColumn({
...commonProps,
id: "remaining_length",
i18ncat: "spool",
unit: "mm",
maxDecimals: 1,
defaultText: t("unknown"),
actions,
dataSource,
tableState,
width: 120,
sorter: true,
}),
FilteredQueryColumn({
...commonProps,
id: "location",
i18ncat: "spool",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanLocations(),
width: 120,
sorter: true,
}),
FilteredQueryColumn({
...commonProps,
id: "lot_nr",
i18ncat: "spool",
actions,
dataSource,
tableState,
filterValueQuery: useSpoolmanLotNumbers(),
width: 120,
sorter: true,
}),
DateColumn({
...commonProps,
id: "first_used",
i18ncat: "spool",
actions,
dataSource,
tableState,
sorter: true,
}),
DateColumn({
...commonProps,
id: "last_used",
i18ncat: "spool",
actions,
dataSource,
tableState,
sorter: true,
}),
DateColumn({
...commonProps,
id: "registered",
i18ncat: "spool",
actions,
dataSource,
tableState,
sorter: true,
}),
...(extraFields.data?.map((field) => {
return CustomFieldColumn({
...commonProps,
field,
});
}) ?? []),
RichColumn({
...commonProps,
id: "comment",
i18ncat: "spool",
actions,
dataSource,
tableState,
width: 150,
sorter: true,
}),
ActionsColumn(actions),
])}

View File

@@ -15,4 +15,8 @@ export interface ISpool {
lot_nr?: string;
comment?: string;
archived: boolean;
extra: { [key: string]: string };
}
// ISpoolParsedExtras is the same as ISpool, but with the extra field parsed into its real types
export type ISpoolParsedExtras = Omit<ISpool, "extra"> & { extra?: { [key: string]: unknown } };

View File

@@ -8,6 +8,8 @@ import utc from "dayjs/plugin/utc";
import { ISpool } from "./model";
import { enrichText } from "../../utils/parsing";
import { IFilament } from "../filaments/model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldDisplay } from "../../components/extraFields";
dayjs.extend(utc);
@@ -15,6 +17,7 @@ const { Title } = Typography;
export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.spool);
const { queryResult } = useShow<ISpool>({
liveMode: "auto",
@@ -25,12 +28,12 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
const spoolPrice = (item: ISpool) => {
let spoolPrice = "";
if (!item.price){
if (!item.price) {
spoolPrice = `${item.filament.price}`;
return spoolPrice;
}
return item.price;
}
};
const formatFilament = (item: IFilament) => {
let vendorPrefix = "";
@@ -133,6 +136,10 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={enrichText(record?.comment)} />
<Title level={5}>{t("spool.fields.archived")}</Title>
<TextField value={record?.archived ? t("yes") : t("no")} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>
{extraFields?.data?.map((field, index) => (
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />
))}
</Show>
);
};