feat: Add inline creation modals for filament and vendor (#1)
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
When creating a spool, users can now create a new filament directly from the filament dropdown without leaving the page. Similarly, when creating a filament, users can create a new vendor inline. New components: - VendorCreateModal: Simple modal for quick vendor creation - FilamentCreateModal: Modal with essential fields + nested vendor creation Changes: - Added dropdownRender to filament Select in spool create page - Added dropdownRender to vendor Select in filament create page - Added translation keys for success messages and placeholders - Pre-fills sensible defaults (diameter 1.75mm, density 1.24 g/cm³) Closes #1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,7 @@
|
|||||||
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
|
"name": "Filament name, to distinguish this filament type among others from the same manufacturer. Should contain the color for example.",
|
||||||
"material": "E.g. PLA, ABS, PETG, etc.",
|
"material": "E.g. PLA, ABS, PETG, etc.",
|
||||||
"price": "Price of a full spool.",
|
"price": "Price of a full spool.",
|
||||||
|
"density": "Auto-filled based on material. Standard: PLA 1.24, ABS 1.04, PETG 1.27 g/cm³.",
|
||||||
"weight": "The filament weight of a full spool (net weight). This should not include the weight of the spool itself, only the filament. It is what is usually written on the packaging.",
|
"weight": "The filament weight of a full spool (net weight). This should not include the weight of the spool itself, only the filament. It is what is usually written on the packaging.",
|
||||||
"spool_weight": "The weight of an empty spool. Used to determine measured weight of a spool.",
|
"spool_weight": "The weight of an empty spool. Used to determine measured weight of a spool.",
|
||||||
"article_number": "E.g. EAN, UPC, etc.",
|
"article_number": "E.g. EAN, UPC, etc.",
|
||||||
@@ -259,7 +260,10 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
|
"filament_updated": "This filament has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
|
||||||
"import_external": "Import from External",
|
"import_external": "Import from External",
|
||||||
"import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.<br><br>A Manufacturer object will be created automatically when you click Ok, if necessary."
|
"import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.<br><br>A Manufacturer object will be created automatically when you click Ok, if necessary.",
|
||||||
|
"created_success": "Created filament \"{{name}}\"",
|
||||||
|
"vendor_placeholder": "Select a manufacturer",
|
||||||
|
"name_placeholder": "E.g. Matte Black"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"add_spool": "Add Spool"
|
"add_spool": "Add Spool"
|
||||||
@@ -287,7 +291,9 @@
|
|||||||
"show_title": "[Manufacturer #{{id}}] {{name}}"
|
"show_title": "[Manufacturer #{{id}}] {{name}}"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!"
|
"vendor_updated": "This manufacturer has been updated by someone/something else since you opened this page. Saving will overwrite those changes!",
|
||||||
|
"created_success": "Created manufacturer \"{{name}}\"",
|
||||||
|
"name_required": "Name is required"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
|||||||
212
client/src/components/filamentCreateModal.tsx
Normal file
212
client/src/components/filamentCreateModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { useSelect } from "@refinedev/antd";
|
||||||
|
import { useInvalidate, useTranslate } from "@refinedev/core";
|
||||||
|
import { Button, ColorPicker, Divider, Form, Input, InputNumber, Modal, Select, message } from "antd";
|
||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Color } from "antd/es/color-picker";
|
||||||
|
import { IFilament } from "../pages/filaments/model";
|
||||||
|
import { IVendor } from "../pages/vendors/model";
|
||||||
|
import { getAPIURL } from "../utils/url";
|
||||||
|
import { VendorCreateModal } from "./vendorCreateModal";
|
||||||
|
|
||||||
|
export interface FilamentCreateModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onSuccess: (filament: IFilament) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values for common filament properties
|
||||||
|
const DEFAULT_DIAMETER = 1.75;
|
||||||
|
const DEFAULT_DENSITY = 1.24; // PLA
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal for quickly creating a filament inline without navigating away from the current page.
|
||||||
|
* Includes essential fields and the ability to create a vendor inline.
|
||||||
|
*/
|
||||||
|
export function FilamentCreateModal(props: FilamentCreateModalProps) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const t = useTranslate();
|
||||||
|
const invalidate = useInvalidate();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isVendorModalOpen, setIsVendorModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Vendor select
|
||||||
|
const { selectProps: vendorSelectProps } = useSelect<IVendor>({
|
||||||
|
resource: "vendor",
|
||||||
|
optionLabel: "name",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleVendorCreated = async (vendor: IVendor) => {
|
||||||
|
// Invalidate vendor cache so select options refresh
|
||||||
|
await invalidate({
|
||||||
|
resource: "vendor",
|
||||||
|
invalidates: ["list"],
|
||||||
|
});
|
||||||
|
form.setFieldValue("vendor_id", vendor.id);
|
||||||
|
setIsVendorModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Convert ColorPicker value to hex string if present
|
||||||
|
let colorHex: string | undefined = undefined;
|
||||||
|
if (values.color_hex) {
|
||||||
|
const color = values.color_hex as Color;
|
||||||
|
colorHex = typeof color === "string" ? color : color.toHexString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
name: values.name || undefined,
|
||||||
|
vendor_id: values.vendor_id || undefined,
|
||||||
|
material: values.material || undefined,
|
||||||
|
density: values.density,
|
||||||
|
diameter: values.diameter,
|
||||||
|
weight: values.weight || undefined,
|
||||||
|
color_hex: colorHex,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(getAPIURL() + "/filament", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || "Failed to create filament");
|
||||||
|
}
|
||||||
|
|
||||||
|
const filament: IFilament = await response.json();
|
||||||
|
|
||||||
|
// Invalidate filament cache so lists refresh
|
||||||
|
await invalidate({
|
||||||
|
resource: "filament",
|
||||||
|
invalidates: ["list"],
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success(t("filament.form.created_success", { name: filament.name || `#${filament.id}` }));
|
||||||
|
|
||||||
|
props.onSuccess(filament);
|
||||||
|
form.resetFields();
|
||||||
|
props.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : "Failed to create filament");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set default values when modal opens
|
||||||
|
const handleAfterOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
diameter: DEFAULT_DIAMETER,
|
||||||
|
density: DEFAULT_DENSITY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VendorCreateModal
|
||||||
|
isOpen={isVendorModalOpen}
|
||||||
|
onSuccess={handleVendorCreated}
|
||||||
|
onClose={() => setIsVendorModalOpen(false)}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
title={t("filament.titles.create")}
|
||||||
|
open={props.isOpen}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
confirmLoading={isSubmitting}
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
|
destroyOnClose
|
||||||
|
width={500}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
{/* Vendor with inline create option */}
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.vendor")}
|
||||||
|
name="vendor_id"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...vendorSelectProps}
|
||||||
|
allowClear
|
||||||
|
placeholder={t("filament.form.vendor_placeholder")}
|
||||||
|
dropdownRender={(menu) => (
|
||||||
|
<>
|
||||||
|
{menu}
|
||||||
|
<Divider style={{ margin: "8px 0" }} />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setIsVendorModalOpen(true)}
|
||||||
|
style={{ width: "100%", textAlign: "left" }}
|
||||||
|
>
|
||||||
|
{t("vendor.titles.create")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.name")}
|
||||||
|
name="name"
|
||||||
|
>
|
||||||
|
<Input maxLength={64} placeholder={t("filament.form.name_placeholder")} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.material")}
|
||||||
|
name="material"
|
||||||
|
>
|
||||||
|
<Input maxLength={64} placeholder="PLA, PETG, ABS, etc." />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.color_hex")}
|
||||||
|
name="color_hex"
|
||||||
|
>
|
||||||
|
<ColorPicker format="hex" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.density")}
|
||||||
|
name="density"
|
||||||
|
rules={[{ required: true, type: "number", min: 0.01, max: 100 }]}
|
||||||
|
help={t("filament.fields_help.density")}
|
||||||
|
>
|
||||||
|
<InputNumber addonAfter="g/cm³" precision={2} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.diameter")}
|
||||||
|
name="diameter"
|
||||||
|
rules={[{ required: true, type: "number", min: 0.01, max: 10 }]}
|
||||||
|
>
|
||||||
|
<InputNumber addonAfter="mm" precision={2} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("filament.fields.weight")}
|
||||||
|
name="weight"
|
||||||
|
help={t("filament.fields_help.weight")}
|
||||||
|
>
|
||||||
|
<InputNumber addonAfter="g" precision={0} min={0} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
client/src/components/vendorCreateModal.tsx
Normal file
98
client/src/components/vendorCreateModal.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useTranslate } from "@refinedev/core";
|
||||||
|
import { Form, Input, InputNumber, Modal, message } from "antd";
|
||||||
|
import TextArea from "antd/es/input/TextArea";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { IVendor } from "../pages/vendors/model";
|
||||||
|
import { getAPIURL } from "../utils/url";
|
||||||
|
|
||||||
|
export interface VendorCreateModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onSuccess: (vendor: IVendor) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal for quickly creating a vendor inline without navigating away from the current page.
|
||||||
|
* Only requires the vendor name - all other fields are optional.
|
||||||
|
*/
|
||||||
|
export function VendorCreateModal(props: VendorCreateModalProps) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const t = useTranslate();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const body: Omit<IVendor, "id" | "registered" | "extra"> = {
|
||||||
|
name: values.name,
|
||||||
|
comment: values.comment || undefined,
|
||||||
|
empty_spool_weight: values.empty_spool_weight || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(getAPIURL() + "/vendor", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || "Failed to create vendor");
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendor: IVendor = await response.json();
|
||||||
|
message.success(t("vendor.form.created_success", { name: vendor.name }));
|
||||||
|
|
||||||
|
props.onSuccess(vendor);
|
||||||
|
form.resetFields();
|
||||||
|
props.onClose();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : "Failed to create vendor");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t("vendor.titles.create")}
|
||||||
|
open={props.isOpen}
|
||||||
|
onOk={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
confirmLoading={isSubmitting}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
label={t("vendor.fields.name")}
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: t("vendor.form.name_required") }]}
|
||||||
|
>
|
||||||
|
<Input maxLength={64} autoFocus />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vendor.fields.comment")}
|
||||||
|
name="comment"
|
||||||
|
>
|
||||||
|
<TextArea maxLength={1024} rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("vendor.fields.empty_spool_weight")}
|
||||||
|
name="empty_spool_weight"
|
||||||
|
help={t("vendor.fields_help.empty_spool_weight")}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} addonAfter="g" precision={1} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Create, useForm, useSelect } from "@refinedev/antd";
|
import { Create, useForm, useSelect } from "@refinedev/antd";
|
||||||
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
||||||
import { Button, ColorPicker, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
import { Button, ColorPicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||||
import { FilamentImportModal } from "../../components/filamentImportModal";
|
import { FilamentImportModal } from "../../components/filamentImportModal";
|
||||||
|
import { VendorCreateModal } from "../../components/vendorCreateModal";
|
||||||
import { MultiColorPicker } from "../../components/multiColorPicker";
|
import { MultiColorPicker } from "../../components/multiColorPicker";
|
||||||
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
|
||||||
import { ExternalFilament } from "../../utils/queryExternalDB";
|
import { ExternalFilament } from "../../utils/queryExternalDB";
|
||||||
@@ -31,6 +33,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
const extraFields = useGetFields(EntityType.filament);
|
const extraFields = useGetFields(EntityType.filament);
|
||||||
const currency = useCurrency();
|
const currency = useCurrency();
|
||||||
const [isImportExtOpen, setIsImportExtOpen] = useState(false);
|
const [isImportExtOpen, setIsImportExtOpen] = useState(false);
|
||||||
|
const [isVendorModalOpen, setIsVendorModalOpen] = useState(false);
|
||||||
const invalidate = useInvalidate();
|
const invalidate = useInvalidate();
|
||||||
const [colorType, setColorType] = useState<"single" | "multi">("single");
|
const [colorType, setColorType] = useState<"single" | "multi">("single");
|
||||||
|
|
||||||
@@ -66,6 +69,16 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
optionLabel: "name",
|
optionLabel: "name",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleVendorCreated = async (vendor: IVendor) => {
|
||||||
|
// Invalidate vendor cache so select options refresh
|
||||||
|
await invalidate({
|
||||||
|
resource: "vendor",
|
||||||
|
invalidates: ["list"],
|
||||||
|
});
|
||||||
|
form.setFieldValue("vendor_id", vendor.id);
|
||||||
|
setIsVendorModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const importFilament = async (filament: ExternalFilament) => {
|
const importFilament = async (filament: ExternalFilament) => {
|
||||||
const vendor = await getOrCreateVendorFromExternal(filament.manufacturer);
|
const vendor = await getOrCreateVendorFromExternal(filament.manufacturer);
|
||||||
await invalidate({
|
await invalidate({
|
||||||
@@ -132,6 +145,11 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
}}
|
}}
|
||||||
onClose={() => setIsImportExtOpen(false)}
|
onClose={() => setIsImportExtOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<VendorCreateModal
|
||||||
|
isOpen={isVendorModalOpen}
|
||||||
|
onSuccess={handleVendorCreated}
|
||||||
|
onClose={() => setIsVendorModalOpen(false)}
|
||||||
|
/>
|
||||||
<Form {...formProps} layout="vertical">
|
<Form {...formProps} layout="vertical">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("filament.fields.name")}
|
label={t("filament.fields.name")}
|
||||||
@@ -165,6 +183,20 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
filterOption={(input, option) =>
|
filterOption={(input, option) =>
|
||||||
typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
|
typeof option?.label === "string" && option?.label.toLowerCase().includes(input.toLowerCase())
|
||||||
}
|
}
|
||||||
|
dropdownRender={(menu) => (
|
||||||
|
<>
|
||||||
|
{menu}
|
||||||
|
<Divider style={{ margin: "8px 0" }} />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setIsVendorModalOpen(true)}
|
||||||
|
style={{ width: "100%", textAlign: "left" }}
|
||||||
|
>
|
||||||
|
{t("vendor.titles.create")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("filament.fields.color_hex")}>
|
<Form.Item label={t("filament.fields.color_hex")}>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
|
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
import { Create, useForm } from "@refinedev/antd";
|
import { Create, useForm } from "@refinedev/antd";
|
||||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
||||||
import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
import { Alert, Button, DatePicker, Divider, Form, Input, InputNumber, Radio, Select, Typography } from "antd";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
|
||||||
|
import { FilamentCreateModal } from "../../components/filamentCreateModal";
|
||||||
import { useSpoolmanLocations } from "../../components/otherModels";
|
import { useSpoolmanLocations } from "../../components/otherModels";
|
||||||
import { searchMatches } from "../../utils/filtering";
|
import { searchMatches } from "../../utils/filtering";
|
||||||
import "../../utils/overrides.css";
|
import "../../utils/overrides.css";
|
||||||
@@ -14,6 +15,7 @@ import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from ".
|
|||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||||
import { createFilamentFromExternal } from "../filaments/functions";
|
import { createFilamentFromExternal } from "../filaments/functions";
|
||||||
|
import { IFilament } from "../filaments/model";
|
||||||
import { useGetFilamentSelectOptions } from "./functions";
|
import { useGetFilamentSelectOptions } from "./functions";
|
||||||
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
|
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
|
||||||
|
|
||||||
@@ -31,6 +33,8 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const extraFields = useGetFields(EntityType.spool);
|
const extraFields = useGetFields(EntityType.spool);
|
||||||
const currency = useCurrency();
|
const currency = useCurrency();
|
||||||
|
const invalidate = useInvalidate();
|
||||||
|
const [isFilamentModalOpen, setIsFilamentModalOpen] = useState(false);
|
||||||
|
|
||||||
const { form, formProps, formLoading, onFinish, redirect } = useForm<
|
const { form, formProps, formLoading, onFinish, redirect } = useForm<
|
||||||
ISpool,
|
ISpool,
|
||||||
@@ -80,6 +84,17 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
allExternalFilaments,
|
allExternalFilaments,
|
||||||
} = useGetFilamentSelectOptions();
|
} = useGetFilamentSelectOptions();
|
||||||
|
|
||||||
|
const handleFilamentCreated = async (filament: IFilament) => {
|
||||||
|
// Invalidate filament cache so the select options refresh
|
||||||
|
await invalidate({
|
||||||
|
resource: "filament",
|
||||||
|
invalidates: ["list"],
|
||||||
|
});
|
||||||
|
// Select the newly created filament
|
||||||
|
form.setFieldValue("filament_id", filament.id);
|
||||||
|
setIsFilamentModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const selectedFilamentID = Form.useWatch("filament_id", form);
|
const selectedFilamentID = Form.useWatch("filament_id", form);
|
||||||
const selectedFilament = useMemo(() => {
|
const selectedFilament = useMemo(() => {
|
||||||
// id is a number of it's an internal filament, and a string of it's an external filament.
|
// id is a number of it's an internal filament, and a string of it's an external filament.
|
||||||
@@ -268,6 +283,11 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<FilamentCreateModal
|
||||||
|
isOpen={isFilamentModalOpen}
|
||||||
|
onSuccess={handleFilamentCreated}
|
||||||
|
onClose={() => setIsFilamentModalOpen(false)}
|
||||||
|
/>
|
||||||
<Form {...formProps} layout="vertical">
|
<Form {...formProps} layout="vertical">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("spool.fields.first_used")}
|
label={t("spool.fields.first_used")}
|
||||||
@@ -310,6 +330,20 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
|
|||||||
options={filamentOptions}
|
options={filamentOptions}
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
|
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
|
||||||
|
dropdownRender={(menu) => (
|
||||||
|
<>
|
||||||
|
{menu}
|
||||||
|
<Divider style={{ margin: "8px 0" }} />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setIsFilamentModalOpen(true)}
|
||||||
|
style={{ width: "100%", textAlign: "left" }}
|
||||||
|
>
|
||||||
|
{t("filament.titles.create")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{selectedFilament?.is_internal === false && (
|
{selectedFilament?.is_internal === false && (
|
||||||
|
|||||||
Reference in New Issue
Block a user