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,21 +1,18 @@
import React from "react";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select, InputNumber, ColorPicker, Button } from "antd";
import { Form, Input, Select, InputNumber, ColorPicker, Button, Typography } 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 { IFilament, IFilamentParsedExtras } from "./model";
import { EntityType, 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";
}
@@ -58,11 +55,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
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);
}
const parsedValue = JSON.parse(field.default_value as string);
form.setFieldsValue({ extra: { [field.key]: parsedValue } });
}
});
@@ -263,6 +256,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
>
<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} />
))}

View File

@@ -1,19 +1,16 @@
import React, { useState } from "react";
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 { Form, Input, DatePicker, Select, InputNumber, ColorPicker, message, Alert, Typography } from "antd";
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { IVendor } from "../vendors/model";
import { IFilament } from "./model";
import { IFilament, IFilamentParsedExtras } 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.
@@ -278,6 +275,7 @@ export const FilamentEdit: 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} />
))}

View File

@@ -30,8 +30,6 @@ 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;
}

View File

@@ -18,3 +18,6 @@ export interface IFilament {
color_hex?: string;
extra: { [key: string]: string };
}
// IFilamentParsedExtras is the same as IFilament, but with the extra field parsed into its real types
export type IFilamentParsedExtras = Omit<IFilament, "extra"> & { extra?: { [key: string]: unknown } };

View File

@@ -135,6 +135,7 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={record?.article_number} />
<Title level={5}>{t("filament.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>
{extraFields?.data?.map((field, index) => (
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />
))}

View File

@@ -5,8 +5,8 @@ import utc from "dayjs/plugin/utc";
import { Content } from "antd/es/layout/layout";
import { Menu, theme } from "antd";
import { FileOutlined, HighlightOutlined, SolutionOutlined, ToolOutlined, UserOutlined } from "@ant-design/icons";
import { GeneralSettings } from "./GeneralSettings";
import { ExtraFieldsSettings } from "./ExtraFieldsSettings";
import { GeneralSettings } from "./generalSettings";
import { ExtraFieldsSettings } from "./extraFieldsSettings";
import { Route, Routes, useNavigate } from "react-router-dom";
dayjs.extend(utc);

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>
);
};

View File

@@ -1,9 +1,15 @@
import React from "react";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Create, useForm } from "@refinedev/antd";
import { Button, Form, Input } from "antd";
import { Button, Form, Input, Typography } from "antd";
import TextArea from "antd/es/input/TextArea";
import { IVendor } from "./model";
import { IVendor, IVendorParsedExtras } from "./model";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
interface CreateOrCloneProps {
mode: "create" | "clone";
@@ -11,15 +17,36 @@ interface CreateOrCloneProps {
export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps> = (props) => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.vendor);
const { form, formProps, formLoading, onFinish, redirect } = useForm<IVendor>();
const { form, formProps, formLoading, onFinish, redirect } = useForm<
IVendor,
HttpError,
IVendorParsedExtras,
IVendorParsedExtras
>();
if (!formProps.initialValues) {
formProps.initialValues = {};
}
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 IVendor).id);
};
// 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]);
return (
<Create
title={props.mode === "create" ? t("vendor.titles.create") : t("vendor.titles.clone")}
@@ -58,6 +85,10 @@ export const VendorCreate: React.FC<IResourceComponentsProps & CreateOrCloneProp
>
<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,25 +1,52 @@
import React, { useState } from "react";
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, DatePicker, message, Alert } from "antd";
import { Form, Input, DatePicker, message, Alert, Typography } from "antd";
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { IVendor } from "./model";
import { IVendor, IVendorParsedExtras } from "./model";
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 VendorEdit: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const [messageApi, contextHolder] = message.useMessage();
const [hasChanged, setHasChanged] = useState(false);
const extraFields = useGetFields(EntityType.vendor);
const { formProps, saveButtonProps } = useForm<IVendor>({
const { formProps, saveButtonProps } = useForm<IVendor, HttpError, IVendorParsedExtras, IVendorParsedExtras>({
liveMode: "manual",
onLiveEvent() {
// Warn the user if the vendor has been updated since the form was opened
messageApi.warning(t("vendor.form.vendor_updated"));
setHasChanged(true);
},
queryOptions: {
select(data) {
return {
data: ParsedExtras(data.data),
};
},
},
});
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: IVendorParsedExtras) => {
if (allValues !== undefined && allValues !== null) {
allValues = StringifiedExtras(allValues);
}
originalOnFinish?.(allValues);
};
return (
<Edit saveButtonProps={saveButtonProps}>
{contextHolder}
@@ -71,6 +98,10 @@ export const VendorEdit: 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("vendor.form.vendor_updated")} type="warning" showIcon />}
</Edit>

View File

@@ -7,9 +7,11 @@ import utc from "dayjs/plugin/utc";
import { IVendor } from "./model";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { DateColumn, RichColumn, SortedColumn, ActionsColumn } from "../../components/column";
import { DateColumn, RichColumn, SortedColumn, ActionsColumn, CustomFieldColumn } from "../../components/column";
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);
@@ -20,6 +22,10 @@ const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "com
export const VendorList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const invalidate = useInvalidate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.vendor);
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
// Load initial state
const initialState = useInitialTableState(namespace);
@@ -82,6 +88,15 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("vendor", record.id) },
];
const commonProps = {
t,
navigate,
actions,
dataSource,
tableState,
sorter: true,
};
return (
<List
headerButtons={({ defaultButtons }) => (
@@ -100,10 +115,20 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
<Dropdown
trigger={["click"]}
menu={{
items: allColumns.map((column) => ({
key: column,
label: t(`vendor.fields.${column}`),
})),
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(`vendor.fields.${column_id}`),
};
}),
selectedKeys: showColumns,
selectable: true,
multiple: true,
@@ -132,35 +157,31 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
rowKey="id"
columns={removeUndefined([
SortedColumn({
...commonProps,
id: "id",
i18ncat: "vendor",
actions,
dataSource,
tableState,
width: 70,
}),
SortedColumn({
...commonProps,
id: "name",
i18ncat: "vendor",
actions,
dataSource,
tableState,
}),
DateColumn({
...commonProps,
id: "registered",
i18ncat: "vendor",
actions,
dataSource,
tableState,
sorter: true,
}),
...(extraFields.data?.map((field) => {
return CustomFieldColumn({
...commonProps,
field,
});
}) ?? []),
RichColumn({
...commonProps,
id: "comment",
i18ncat: "vendor",
actions,
dataSource,
tableState,
sorter: true,
}),
ActionsColumn<IVendor>(actions),
])}

View File

@@ -3,4 +3,8 @@ export interface IVendor {
registered: string;
name: string;
comment?: string;
extra: { [key: string]: string };
}
// IVendorParsedExtras is the same as IVendor, but with the extra field parsed into its real types
export type IVendorParsedExtras = Omit<IVendor, "extra"> & { extra?: { [key: string]: unknown } };

View File

@@ -6,6 +6,8 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { IVendor } from "./model";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { ExtraFieldDisplay } from "../../components/extraFields";
dayjs.extend(utc);
@@ -13,6 +15,7 @@ const { Title } = Typography;
export const VendorShow: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.vendor);
const { queryResult } = useShow<IVendor>({
liveMode: "auto",
@@ -39,6 +42,10 @@ export const VendorShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={record?.name} />
<Title level={5}>{t("vendor.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
<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>
);
};