Merge branch 'multicolor'
This commit is contained in:
@@ -205,6 +205,10 @@
|
|||||||
"settings_extruder_temp": "Extruder Temp",
|
"settings_extruder_temp": "Extruder Temp",
|
||||||
"settings_bed_temp": "Bed Temp",
|
"settings_bed_temp": "Bed Temp",
|
||||||
"color_hex": "Color",
|
"color_hex": "Color",
|
||||||
|
"single_color": "Single",
|
||||||
|
"multi_color": "Multi",
|
||||||
|
"coaxial": "Coextruded",
|
||||||
|
"longitudinal": "Longitudinal",
|
||||||
"external_id": "External ID",
|
"external_id": "External ID",
|
||||||
"spools": "Show Spools"
|
"spools": "Show Spools"
|
||||||
},
|
},
|
||||||
@@ -214,7 +218,8 @@
|
|||||||
"price": "Price of a full spool.",
|
"price": "Price of a full spool.",
|
||||||
"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.",
|
||||||
|
"multi_color_direction": "Filaments can have multiple colors in two ways: either through coextrusion, like dual-color filaments with consistent multi-colors, or through longitudinal color changes, like gradient filaments that shift colors along the spool."
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
"create": "Create Filament",
|
"create": "Create Filament",
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import { DateField, TextField } from "@refinedev/antd";
|
import { DateField, TextField } from "@refinedev/antd";
|
||||||
import Icon from "@ant-design/icons";
|
|
||||||
import SpoolIcon from "../icon_spool.svg?react";
|
|
||||||
import { useTranslate } from "@refinedev/core";
|
import { useTranslate } from "@refinedev/core";
|
||||||
import { enrichText } from "../utils/parsing";
|
import { enrichText } from "../utils/parsing";
|
||||||
import { UseQueryResult } from "@tanstack/react-query";
|
import { UseQueryResult } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Field, FieldType } from "../utils/queryFields";
|
import { Field, FieldType } from "../utils/queryFields";
|
||||||
|
import SpoolIcon from "./spoolIcon";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -296,7 +295,7 @@ export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Ac
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SpoolIconColumnProps<Obj extends Entity> extends FilteredQueryColumnProps<Obj> {
|
interface SpoolIconColumnProps<Obj extends Entity> extends FilteredQueryColumnProps<Obj> {
|
||||||
color: (record: Obj) => string | undefined;
|
color: (record: Obj) => string | { colors: string[]; vertical: boolean } | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<Obj>) {
|
export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<Obj>) {
|
||||||
@@ -343,19 +342,12 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
|
|||||||
},
|
},
|
||||||
render: (rawValue, record: Obj) => {
|
render: (rawValue, record: Obj) => {
|
||||||
const value = props.transform ? props.transform(rawValue) : rawValue;
|
const value = props.transform ? props.transform(rawValue) : rawValue;
|
||||||
const colorStr = props.color(record);
|
const colorObj = props.color(record);
|
||||||
return (
|
return (
|
||||||
<Row wrap={false} justify="space-around" align="middle">
|
<Row wrap={false} justify="space-around" align="middle">
|
||||||
{colorStr && (
|
{colorObj && (
|
||||||
<Col flex="none">
|
<Col flex="none">
|
||||||
<Icon
|
<SpoolIcon color={colorObj} />
|
||||||
component={SpoolIcon}
|
|
||||||
style={{
|
|
||||||
color: "#" + colorStr,
|
|
||||||
fontSize: 42,
|
|
||||||
marginRight: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
<Col flex="auto">{value}</Col>
|
<Col flex="auto">{value}</Col>
|
||||||
|
|||||||
85
client/src/components/multiColorPicker.tsx
Normal file
85
client/src/components/multiColorPicker.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { Badge, Button, ColorPicker, InputNumber, Space } from "antd";
|
||||||
|
|
||||||
|
function generateRandomColor() {
|
||||||
|
return "000000".replace(/0/g, function () {
|
||||||
|
return (~~(Math.random() * 16)).toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateInitialColors(num: number) {
|
||||||
|
const colors = [];
|
||||||
|
for (let i = 0; i < num; i++) {
|
||||||
|
colors.push(generateRandomColor());
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Ant Design compatible form input for multiple color pickers
|
||||||
|
* The value is a comma separated list of hex values, without hashtags
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function MultiColorPicker(props: {
|
||||||
|
value?: string | null | undefined;
|
||||||
|
onChange?: (value: string | null | undefined) => void;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}) {
|
||||||
|
const values = props.value ? props.value.split(",") : generateInitialColors(props.min ?? 0);
|
||||||
|
if (!props.value && props.onChange) {
|
||||||
|
// Update value immediately
|
||||||
|
props.onChange(values.join(","));
|
||||||
|
}
|
||||||
|
const pickers = values.map((value, idx) => (
|
||||||
|
<Badge
|
||||||
|
key={idx}
|
||||||
|
count={
|
||||||
|
values.length > (props.min ?? 0) ? (
|
||||||
|
<span className="ant-badge-count">
|
||||||
|
<CloseOutlined
|
||||||
|
onClick={() => {
|
||||||
|
// Remove this picker
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(values.filter((v, i) => i !== idx).join(","));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ColorPicker
|
||||||
|
format="hex"
|
||||||
|
value={value}
|
||||||
|
onChange={(_, newHex) => {
|
||||||
|
if (props.onChange) {
|
||||||
|
newHex = newHex.replace("#", "");
|
||||||
|
props.onChange(values.map((v, i) => (i === idx ? newHex : v)).join(","));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Space direction="horizontal" size="middle" style={{ marginTop: "1em" }}>
|
||||||
|
{pickers}
|
||||||
|
{values.length < (props.max ?? Infinity) && (
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(values.concat(generateRandomColor()).join(","));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
client/src/components/spoolIcon.css
Normal file
41
client/src/components/spoolIcon.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.spool-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
gap: 2px;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.large {
|
||||||
|
width: 4em;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon * {
|
||||||
|
flex: 1 1 0px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.vertical *:first-child {
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.vertical *:last-child {
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.horizontal *:first-child {
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spool-icon.horizontal *:last-child {
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
}
|
||||||
32
client/src/components/spoolIcon.tsx
Normal file
32
client/src/components/spoolIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import "./spoolIcon.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
color: string | { colors: string[]; vertical: boolean };
|
||||||
|
size?: "small" | "large";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpoolIcon(props: Props) {
|
||||||
|
let dirClass = "vertical";
|
||||||
|
let cols = [];
|
||||||
|
let size = props.size ? props.size : "small";
|
||||||
|
|
||||||
|
if (typeof props.color === "string") {
|
||||||
|
cols = [props.color];
|
||||||
|
} else {
|
||||||
|
dirClass = props.color.vertical ? "vertical" : "horizontal";
|
||||||
|
cols = props.color.colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"spool-icon " + dirClass + " " + size}>
|
||||||
|
{cols.map((col) => (
|
||||||
|
<div
|
||||||
|
key={col}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#" + col.replace("#", ""),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" version="1.1" viewBox="0 0 235 500" xml:space="preserve"><desc>Created with Fabric.js 5.3.0</desc><g id="z-gD78pOtWjQii31Q-6Ze"><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.5875748337 0 0 3.9476902309 197.1354865303 250.0467513161) translate(0, 0)" vector-effect="non-scaling-stroke"/></g><g id="qCAOML5j7kpAVIP3Df73i"><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill-rule:nonzero;opacity:1" fill="current" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.3815761198 0 0 3.4623174046 197.1354865303 250.0467382769) translate(0, 0)" vector-effect="non-scaling-stroke"/></g><g id="DwEIaQUSPW4DgKfkdPWoC"><path style="stroke:#000;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill-rule:nonzero;opacity:1" fill="current" stroke-linecap="round" d="M -38.50267 -65.24064 L 38.502680000000005 -65.24064 L 38.502680000000005 65.24064 L -38.50267 65.24064 z" transform="matrix(2.0736420266 0 0 3.3577085856 117.2947215155 250.0467440227) translate(-0.000005, 0)" vector-effect="non-scaling-stroke"/></g><g id="OvgM38-6CZm9R47a1TDIX" transform="matrix(0.5875748337 0 0 3.9476902309 37.4539565008 250.0467440227)"><filter id="SVGID_90" width="181.094%" height="149.483%" x="-40.547%" y="-24.742%"><feGaussianBlur in="SourceAlpha" stdDeviation="20"/><feOffset dx="30" dy="0" result="oBlur"/><feFlood flood-color="#000" flood-opacity=".8"/><feComposite in2="oBlur" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#343434;fill-rule:nonzero;opacity:1;filter:url(#SVGID_90)" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="translate(0, 0)" vector-effect="non-scaling-stroke"/></g><g id="woBV3Wt7ijQ6puNeMgAQD"><path style="stroke:#6e0b30;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#111;fill-rule:nonzero;opacity:1" stroke-linecap="round" d="M 0 -63.26949 C 34.92476 -63.26949 63.26949 -34.92476 63.26949 0 C 63.26949 34.92476 34.92476 63.26949 0 63.26949 C -34.92476 63.26949 -63.26949 34.92476 -63.26949 0 C -63.26949 -34.92476 -34.92476 -63.26949 0 -63.26949 z" transform="matrix(0.2435597948 0 0 1.6363849628 37.4539565008 250.0467440227) translate(0, 0)" vector-effect="non-scaling-stroke"/></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
import { HttpError, IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core";
|
||||||
import { Create, useForm, useSelect } from "@refinedev/antd";
|
import { Create, useForm, useSelect } from "@refinedev/antd";
|
||||||
import { Form, Input, Select, InputNumber, ColorPicker, Button, Typography, Modal } from "antd";
|
import { Form, Input, Select, InputNumber, ColorPicker, Button, Typography, Modal, Radio } from "antd";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import { numberFormatter, numberParser } from "../../utils/parsing";
|
import { numberFormatter, numberParser } from "../../utils/parsing";
|
||||||
import { IVendor } from "../vendors/model";
|
import { IVendor } from "../vendors/model";
|
||||||
@@ -16,6 +16,7 @@ import { ExternalFilament, useGetExternalDBFilaments } from "../../utils/queryEx
|
|||||||
import { formatFilamentLabel } from "../spools/functions";
|
import { formatFilamentLabel } from "../spools/functions";
|
||||||
import { FilamentImportModal } from "../../components/filamentImportModal";
|
import { FilamentImportModal } from "../../components/filamentImportModal";
|
||||||
import { getOrCreateVendorFromExternal } from "../vendors/functions";
|
import { getOrCreateVendorFromExternal } from "../vendors/functions";
|
||||||
|
import { MultiColorPicker } from "../../components/multiColorPicker";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
const currency = useCurrency();
|
const currency = useCurrency();
|
||||||
const [isImportExtOpen, setIsImportExtOpen] = React.useState(false);
|
const [isImportExtOpen, setIsImportExtOpen] = React.useState(false);
|
||||||
const invalidate = useInvalidate();
|
const invalidate = useInvalidate();
|
||||||
|
const [colorType, setColorType] = React.useState<"single" | "multi">("single");
|
||||||
|
|
||||||
const { form, formProps, formLoading, onFinish, redirect } = useForm<
|
const { form, formProps, formLoading, onFinish, redirect } = useForm<
|
||||||
IFilament,
|
IFilament,
|
||||||
@@ -160,20 +162,62 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("filament.fields.color_hex")}>
|
||||||
label={t("filament.fields.color_hex")}
|
<Radio.Group
|
||||||
name={["color_hex"]}
|
onChange={(value) => {
|
||||||
rules={[
|
setColorType(value.target.value);
|
||||||
{
|
}}
|
||||||
required: false,
|
defaultValue={colorType}
|
||||||
},
|
value={colorType}
|
||||||
]}
|
>
|
||||||
getValueFromEvent={(e) => {
|
<Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
|
||||||
return e?.toHex();
|
<Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
|
||||||
}}
|
</Radio.Group>
|
||||||
>
|
|
||||||
<ColorPicker format="hex" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{colorType == "single" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"color_hex"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
getValueFromEvent={(e) => {
|
||||||
|
return e?.toHex();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorPicker format="hex" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{colorType == "multi" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"multi_color_direction"}
|
||||||
|
help={t("filament.fields_help.multi_color_direction")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
initialValue={"coaxial"}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
|
||||||
|
<Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{colorType == "multi" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"multi_color_hexes"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MultiColorPicker min={2} max={14} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("filament.fields.material")}
|
label={t("filament.fields.material")}
|
||||||
help={t("filament.fields_help.material")}
|
help={t("filament.fields_help.material")}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core";
|
||||||
import { Edit, useForm, useSelect } from "@refinedev/antd";
|
import { Edit, useForm, useSelect } from "@refinedev/antd";
|
||||||
import { Form, Input, DatePicker, Select, InputNumber, ColorPicker, message, Alert, Typography } from "antd";
|
import { Form, Input, DatePicker, Select, InputNumber, ColorPicker, message, Alert, Typography, Radio } from "antd";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import { numberFormatter, numberParser } from "../../utils/parsing";
|
import { numberFormatter, numberParser } from "../../utils/parsing";
|
||||||
@@ -11,6 +11,7 @@ import { EntityType, useGetFields } from "../../utils/queryFields";
|
|||||||
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
|
import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields";
|
||||||
import { ParsedExtras } from "../../components/extraFields";
|
import { ParsedExtras } from "../../components/extraFields";
|
||||||
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
|
||||||
|
import { MultiColorPicker } from "../../components/multiColorPicker";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The API returns the extra fields as JSON values, but we need to parse them into their real types
|
The API returns the extra fields as JSON values, but we need to parse them into their real types
|
||||||
@@ -25,6 +26,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
|
|||||||
const [hasChanged, setHasChanged] = useState(false);
|
const [hasChanged, setHasChanged] = useState(false);
|
||||||
const extraFields = useGetFields(EntityType.filament);
|
const extraFields = useGetFields(EntityType.filament);
|
||||||
const currency = useCurrency();
|
const currency = useCurrency();
|
||||||
|
const [colorType, setColorType] = React.useState<"single" | "multi">("single");
|
||||||
|
|
||||||
const { formProps, saveButtonProps } = useForm<IFilament, HttpError, IFilament, IFilament>({
|
const { formProps, saveButtonProps } = useForm<IFilament, HttpError, IFilament, IFilament>({
|
||||||
liveMode: "manual",
|
liveMode: "manual",
|
||||||
@@ -49,6 +51,16 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
|
|||||||
formProps.initialValues = ParsedExtras(formProps.initialValues);
|
formProps.initialValues = ParsedExtras(formProps.initialValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update colorType state
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log(formProps.initialValues?.multi_color_hexes);
|
||||||
|
if (formProps.initialValues?.multi_color_hexes) {
|
||||||
|
setColorType("multi");
|
||||||
|
} else {
|
||||||
|
setColorType("single");
|
||||||
|
}
|
||||||
|
}, [formProps.initialValues?.multi_color_hexes]);
|
||||||
|
|
||||||
// Override the form's onFinish method to stringify the extra fields
|
// Override the form's onFinish method to stringify the extra fields
|
||||||
const originalOnFinish = formProps.onFinish;
|
const originalOnFinish = formProps.onFinish;
|
||||||
formProps.onFinish = (allValues: IFilamentParsedExtras) => {
|
formProps.onFinish = (allValues: IFilamentParsedExtras) => {
|
||||||
@@ -132,20 +144,62 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("filament.fields.color_hex")}>
|
||||||
label={t("filament.fields.color_hex")}
|
<Radio.Group
|
||||||
name={["color_hex"]}
|
onChange={(value) => {
|
||||||
rules={[
|
setColorType(value.target.value);
|
||||||
{
|
}}
|
||||||
required: false,
|
defaultValue={colorType}
|
||||||
},
|
value={colorType}
|
||||||
]}
|
>
|
||||||
getValueFromEvent={(e) => {
|
<Radio.Button value={"single"}>{t("filament.fields.single_color")}</Radio.Button>
|
||||||
return e?.toHex();
|
<Radio.Button value={"multi"}>{t("filament.fields.multi_color")}</Radio.Button>
|
||||||
}}
|
</Radio.Group>
|
||||||
>
|
|
||||||
<ColorPicker format="hex" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{colorType == "single" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"color_hex"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
getValueFromEvent={(e) => {
|
||||||
|
return e?.toHex();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColorPicker format="hex" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{colorType == "multi" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"multi_color_direction"}
|
||||||
|
help={t("filament.fields_help.multi_color_direction")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
initialValue={"coaxial"}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio.Button value={"coaxial"}>{t("filament.fields.coaxial")}</Radio.Button>
|
||||||
|
<Radio.Button value={"longitudinal"}>{t("filament.fields.longitudinal")}</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{colorType == "multi" && (
|
||||||
|
<Form.Item
|
||||||
|
name={"multi_color_hexes"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MultiColorPicker min={2} max={14} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("filament.fields.material")}
|
label={t("filament.fields.material")}
|
||||||
help={t("filament.fields_help.material")}
|
help={t("filament.fields_help.material")}
|
||||||
|
|||||||
@@ -240,7 +240,13 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
...commonProps,
|
...commonProps,
|
||||||
id: "name",
|
id: "name",
|
||||||
i18ncat: "filament",
|
i18ncat: "filament",
|
||||||
color: (record: IFilamentCollapsed) => record.color_hex,
|
color: (record: IFilamentCollapsed) =>
|
||||||
|
record.multi_color_hexes
|
||||||
|
? {
|
||||||
|
colors: record.multi_color_hexes.split(","),
|
||||||
|
vertical: record.multi_color_direction === "longitudinal",
|
||||||
|
}
|
||||||
|
: record.color_hex,
|
||||||
filterValueQuery: useSpoolmanFilamentNames(),
|
filterValueQuery: useSpoolmanFilamentNames(),
|
||||||
}),
|
}),
|
||||||
FilteredQueryColumn({
|
FilteredQueryColumn({
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface IFilament {
|
|||||||
settings_extruder_temp?: number;
|
settings_extruder_temp?: number;
|
||||||
settings_bed_temp?: number;
|
settings_bed_temp?: number;
|
||||||
color_hex?: string;
|
color_hex?: string;
|
||||||
|
multi_color_hexes?: string;
|
||||||
|
multi_color_direction?: string;
|
||||||
external_id?: string;
|
external_id?: string;
|
||||||
extra: { [key: string]: string };
|
extra: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
import { ExtraFieldDisplay } from "../../components/extraFields";
|
import { ExtraFieldDisplay } from "../../components/extraFields";
|
||||||
import { useCurrency } from "../../utils/settings";
|
import { useCurrency } from "../../utils/settings";
|
||||||
|
import SpoolIcon from "../../components/spoolIcon";
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
@@ -49,6 +50,13 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
navigate(URL);
|
navigate(URL);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const colorObj = record?.multi_color_hexes
|
||||||
|
? {
|
||||||
|
colors: record.multi_color_hexes.split(","),
|
||||||
|
vertical: record.multi_color_direction === "longitudinal",
|
||||||
|
}
|
||||||
|
: record?.color_hex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -80,7 +88,7 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
<Title level={5}>{t("filament.fields.name")}</Title>
|
<Title level={5}>{t("filament.fields.name")}</Title>
|
||||||
<TextField value={record?.name} />
|
<TextField value={record?.name} />
|
||||||
<Title level={5}>{t("filament.fields.color_hex")}</Title>
|
<Title level={5}>{t("filament.fields.color_hex")}</Title>
|
||||||
<TextField value={record?.color_hex} />
|
{colorObj && <SpoolIcon color={colorObj} size="large" />}
|
||||||
<Title level={5}>{t("filament.fields.material")}</Title>
|
<Title level={5}>{t("filament.fields.material")}</Title>
|
||||||
<TextField value={record?.material} />
|
<TextField value={record?.material} />
|
||||||
<Title level={5}>{t("filament.fields.price")}</Title>
|
<Title level={5}>{t("filament.fields.price")}</Title>
|
||||||
|
|||||||
@@ -342,7 +342,13 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
...commonProps,
|
...commonProps,
|
||||||
id: "filament.combined_name",
|
id: "filament.combined_name",
|
||||||
i18nkey: "spool.fields.filament_name",
|
i18nkey: "spool.fields.filament_name",
|
||||||
color: (record: ISpoolCollapsed) => record.filament.color_hex,
|
color: (record: ISpoolCollapsed) =>
|
||||||
|
record.filament.multi_color_hexes
|
||||||
|
? {
|
||||||
|
colors: record.filament.multi_color_hexes.split(","),
|
||||||
|
vertical: record.filament.multi_color_direction === "longitudinal",
|
||||||
|
}
|
||||||
|
: record.filament.color_hex,
|
||||||
dataId: "filament.combined_name",
|
dataId: "filament.combined_name",
|
||||||
filterValueQuery: useSpoolmanFilamentFilter(),
|
filterValueQuery: useSpoolmanFilamentFilter(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { IFilament } from "../filaments/model";
|
|||||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||||
import { ExtraFieldDisplay } from "../../components/extraFields";
|
import { ExtraFieldDisplay } from "../../components/extraFields";
|
||||||
import { useCurrency } from "../../utils/settings";
|
import { useCurrency } from "../../utils/settings";
|
||||||
|
import SpoolIcon from "../../components/spoolIcon";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -66,12 +67,19 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const colorObj = record?.filament.multi_color_hexes
|
||||||
|
? {
|
||||||
|
colors: record.filament.multi_color_hexes.split(","),
|
||||||
|
vertical: record.filament.multi_color_direction === "longitudinal",
|
||||||
|
}
|
||||||
|
: record?.filament.color_hex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading} title={record ? formatTitle(record) : ""}>
|
<Show isLoading={isLoading} title={record ? formatTitle(record) : ""}>
|
||||||
<Title level={5}>{t("spool.fields.id")}</Title>
|
<Title level={5}>{t("spool.fields.id")}</Title>
|
||||||
<NumberField value={record?.id ?? ""} />
|
<NumberField value={record?.id ?? ""} />
|
||||||
<Title level={5}>{t("spool.fields.filament")}</Title>
|
<Title level={5}>{t("spool.fields.filament")}</Title>
|
||||||
<TextField value={record ? filamentURL(record?.filament) : ""} />
|
{colorObj && <SpoolIcon color={colorObj} />} <TextField value={record ? filamentURL(record?.filament) : ""} />
|
||||||
<Title level={5}>{t("spool.fields.price")}</Title>
|
<Title level={5}>{t("spool.fields.price")}</Title>
|
||||||
<NumberField
|
<NumberField
|
||||||
value={record ? spoolPrice(record) : ""}
|
value={record ? spoolPrice(record) : ""}
|
||||||
|
|||||||
Reference in New Issue
Block a user