Merge branch 'master' into adjust-spool-button-on-show

This commit is contained in:
Charalampos Tsachakis
2025-03-19 09:07:08 +02:00
committed by GitHub
62 changed files with 3668 additions and 1888 deletions

View File

@@ -15,12 +15,12 @@ import {
UserOutlined,
} from "@ant-design/icons";
import loadable from "@loadable/component";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router-v6";
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
import { ConfigProvider } from "antd";
import { Locale } from "antd/es/locale";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import dataProvider from "./components/dataProvider";
import { Favicon } from "./components/favicon";
import { SpoolmanLayout } from "./components/layout";

View File

@@ -5,7 +5,7 @@ import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { AlignType } from "rc-table/lib/interface";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { getFiltersForField, typeFilters } from "../utils/filtering";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
@@ -111,11 +111,13 @@ function Column<Obj extends Entity>(
if (props.loadingFilters) {
columnProps.filterDropdown = <FilterDropdownLoading />;
}
columnProps.onFilterDropdownOpenChange = (open) => {
if (open && props.onFilterDropdownOpen) {
props.onFilterDropdownOpen();
columnProps.filterDropdownProps = {
onOpenChange: (open) => {
if (open && props.onFilterDropdownOpen) {
props.onFilterDropdownOpen();
}
}
};
}
if (props.dataId) {
columnProps.key = props.dataId;
}

View File

@@ -7,7 +7,6 @@ import { ColorModeContext } from "../../contexts/color-mode";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
import "/node_modules/flag-icons/css/flag-icons.min.css";
const { useToken } = theme;
@@ -22,7 +21,6 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) =>
const menuItems: MenuProps["items"] = [...(Object.keys(languages) || [])].sort().map((lang: string) => ({
key: lang,
onClick: () => changeLanguage(lang),
icon: <span className={"fi fi-" + languages[lang].countryCode} style={{ marginRight: 8 }} />,
label: languages[lang].name,
}));
@@ -52,7 +50,6 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) =>
>
<Button type="text">
<Space>
<span className={"fi fi-" + languages[currentLocale ?? "en"].countryCode} />
{languages[currentLocale ?? "en"].name}
<DownOutlined />
</Space>

View File

@@ -3,7 +3,7 @@ import { useTranslate } from "@refinedev/core";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { FloatButton, Modal, Space } from "antd";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
const QRCodeScannerModal: React.FC = () => {
const [visible, setVisible] = useState(false);

View File

@@ -15,6 +15,10 @@
height: 4em;
}
.spool-icon.no-margin {
margin: 0;
}
.spool-icon * {
flex: 1 1 0px;
border-radius: 2px;

View File

@@ -3,13 +3,15 @@ import "./spoolIcon.css";
interface Props {
color: string | { colors: string[]; vertical: boolean };
size?: "small" | "large";
no_margin? : boolean
}
export default function SpoolIcon(props: Props) {
export default function SpoolIcon(props: Readonly<Props>) {
let dirClass = "vertical";
let cols = [];
let size = props.size ? props.size : "small";
let no_margin = props.no_margin ? "no-margin" : "";
if (typeof props.color === "string") {
cols = [props.color];
} else {
@@ -18,7 +20,7 @@ export default function SpoolIcon(props: Props) {
}
return (
<div className={"spool-icon " + dirClass + " " + size}>
<div className={"spool-icon " + dirClass + " " + size + " " + no_margin}>
{cols.map((col) => (
<div
key={col}

View File

@@ -7,7 +7,6 @@ import { getBasePath } from "./utils/url";
interface Language {
name: string;
countryCode: string;
fullCode: string;
djs: () => Promise<ILocale>;
}
@@ -16,131 +15,130 @@ interface Language {
* List of languages to load
* The key of each object is the folder name in the locales dir.
* name: Name of the language in the list
* countryCode: Country code of the country's flag to display for this language
* fullCode: Full language code, used for Ant Design's locale
* djs: Function to load the dayjs locale, see https://github.com/iamkun/dayjs/tree/dev/src/locale for list of locales
*/
export const languages: { [key: string]: Language } = {
["en"]: {
name: "English",
countryCode: "gb",
fullCode: "en-GB",
djs: () => import("dayjs/locale/en"),
},
["sv"]: {
name: "Svenska",
countryCode: "se",
fullCode: "sv-SE",
djs: () => import("dayjs/locale/sv"),
},
["de"]: {
name: "Deutsch",
countryCode: "de",
fullCode: "de-DE",
djs: () => import("dayjs/locale/de"),
},
["es"]: {
name: "Español",
countryCode: "es",
fullCode: "es-ES",
djs: () => import("dayjs/locale/es"),
},
["zh"]: {
name: "简体中文",
countryCode: "cn",
fullCode: "zh-CN",
djs: () => import("dayjs/locale/zh-cn"),
},
["zh-Hant"]: {
name: "繁體中文",
countryCode: "cn",
fullCode: "zh-TW",
djs: () => import("dayjs/locale/zh-hk"),
},
["pl"]: {
name: "Polski",
countryCode: "pl",
fullCode: "pl-PL",
djs: () => import("dayjs/locale/pl"),
},
["ru"]: {
name: "Русский",
countryCode: "ru",
fullCode: "ru-RU",
djs: () => import("dayjs/locale/ru"),
},
["cs"]: {
name: "Česky",
countryCode: "cz",
fullCode: "cs-CZ",
djs: () => import("dayjs/locale/cs"),
},
["nb-NO"]: {
name: "Norsk bokmål",
countryCode: "no",
fullCode: "nb-NO",
djs: () => import("dayjs/locale/nb"),
},
["nl"]: {
name: "Nederlands",
countryCode: "nl",
fullCode: "nl-NL",
djs: () => import("dayjs/locale/nl"),
},
["fr"]: {
name: "Français",
countryCode: "fr",
fullCode: "fr-FR",
djs: () => import("dayjs/locale/fr"),
},
["hu"]: {
name: "Magyar",
countryCode: "hu",
fullCode: "hu-HU",
djs: () => import("dayjs/locale/hu"),
},
["it"]: {
name: "Italiano",
countryCode: "it",
fullCode: "it-IT",
djs: () => import("dayjs/locale/it"),
},
["uk"]: {
name: "Українська",
countryCode: "ua",
fullCode: "uk-UA",
djs: () => import("dayjs/locale/uk"),
},
["el"]: {
name: "Ελληνικά",
countryCode: "gr",
fullCode: "el-GR",
djs: () => import("dayjs/locale/el"),
},
["da"]: {
name: "Dansk",
countryCode: "dk",
fullCode: "da-DK",
djs: () => import("dayjs/locale/da"),
},
["pt"]: {
name: "Português",
countryCode: "pt",
fullCode: "pt-PT",
djs: () => import("dayjs/locale/pt"),
},
["fa"]: {
name: "فارسی",
countryCode: "ir",
fullCode: "fa-IR",
djs: () => import("dayjs/locale/fa"),
},
["ro"]: {
name: "Român",
countryCode: "ro",
fullCode: "ro-RO",
djs: () => import("dayjs/locale/ro"),
},
["ja"]: {
name: "日本語",
fullCode: "ja-JP",
djs: () => import("dayjs/locale/ja"),
},
["pt-BR"]: {
name: "Português (Brasil)",
fullCode: "pt-BR",
djs: () => import("dayjs/locale/pt-br"),
},
["ta"]: {
name: "தமிழ்",
fullCode: "ta-IN",
djs: () => import("dayjs/locale/ta"),
},
["th"]: {
name: "ไทย",
fullCode: "th-TH",
djs: () => import("dayjs/locale/th"),
},
};
i18n

View File

@@ -1,3 +1,4 @@
import '@ant-design/v5-patch-for-react-19';
import React from "react";
import { createRoot } from "react-dom/client";

View File

@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { FilamentImportModal } from "../../components/filamentImportModal";
import { MultiColorPicker } from "../../components/multiColorPicker";
import { numberFormatter, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { ExternalFilament } from "../../utils/queryExternalDB";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
@@ -250,7 +250,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
<InputNumber
addonAfter={getCurrencySymbol(undefined, currency)}
precision={2}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParserAllowEmpty}
/>
</Form.Item>
@@ -266,7 +266,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
},
]}
>
<InputNumber addonAfter="g/cm³" precision={2} formatter={numberFormatter} parser={numberParser} />
<InputNumber addonAfter="g/cm³" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
</Form.Item>
<Form.Item
label={t("filament.fields.diameter")}
@@ -280,7 +280,7 @@ export const FilamentCreate: React.FC<IResourceComponentsProps & CreateOrClonePr
},
]}
>
<InputNumber addonAfter="mm" precision={2} formatter={numberFormatter} parser={numberParser} />
<InputNumber addonAfter="mm" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
</Form.Item>
<Form.Item
label={t("filament.fields.weight")}

View File

@@ -6,7 +6,7 @@ import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { MultiColorPicker } from "../../components/multiColorPicker";
import { numberFormatter, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { IVendor } from "../vendors/model";
@@ -63,6 +63,9 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: IFilamentParsedExtras) => {
if (allValues !== undefined && allValues !== null) {
if (colorType == "single") {
allValues.multi_color_hexes = "";
}
// Lot of stupidity here to make types work
const stringifiedAllValues = StringifiedExtras<IFilamentParsedExtras>(allValues);
originalOnFinish?.({
@@ -166,7 +169,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
return e?.toHex();
}}
>
<ColorPicker format="hex" />
<ColorPicker />
</Form.Item>
)}
{colorType == "multi" && (
@@ -225,7 +228,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
<InputNumber
addonAfter={getCurrencySymbol(undefined, currency)}
precision={2}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParserAllowEmpty}
/>
</Form.Item>
@@ -241,7 +244,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
},
]}
>
<InputNumber addonAfter="g/cm³" precision={2} formatter={numberFormatter} parser={numberParser} />
<InputNumber addonAfter="g/cm³" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
</Form.Item>
<Form.Item
label={t("filament.fields.diameter")}
@@ -255,7 +258,7 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
},
]}
>
<InputNumber addonAfter="mm" precision={2} formatter={numberFormatter} parser={numberParser} />
<InputNumber addonAfter="mm" precision={2} formatter={formatNumberOnUserInput} parser={numberParser} />
</Form.Item>
<Form.Item
label={t("filament.fields.weight")}

View File

@@ -5,28 +5,28 @@ import { Button, Dropdown, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import {
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
} from "../../components/column";
import { useLiveify } from "../../components/liveify";
import {
useSpoolmanArticleNumbers,
useSpoolmanFilamentNames,
useSpoolmanMaterials,
useSpoolmanVendors,
useSpoolmanArticleNumbers,
useSpoolmanFilamentNames,
useSpoolmanMaterials,
useSpoolmanVendors,
} from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { useCurrency } from "../../utils/settings";
import { useCurrencyFormatter } from "../../utils/settings";
import { IFilament } from "./model";
dayjs.extend(utc);
@@ -77,7 +77,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
const invalidate = useInvalidate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.filament);
const currency = useCurrency();
const currencyFormatter = useCurrencyFormatter();
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
@@ -263,12 +263,10 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
align: "right",
width: 80,
render: (_, obj: IFilamentCollapsed) => {
return obj.price?.toLocaleString(undefined, {
style: "currency",
currencyDisplay: "narrowSymbol",
currency: currency,
notation: "compact",
});
if (obj.price === undefined) {
return "";
}
return currencyFormatter.format(obj.price);
},
}),
NumberColumn({

View File

@@ -4,13 +4,13 @@ import { Button, Typography } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { ExtraFieldDisplay } from "../../components/extraFields";
import { NumberFieldUnit } from "../../components/numberField";
import SpoolIcon from "../../components/spoolIcon";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrency } from "../../utils/settings";
import { useCurrencyFormatter } from "../../utils/settings";
import { IFilament } from "./model";
dayjs.extend(utc);
@@ -20,7 +20,7 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.filament);
const currency = useCurrency();
const currencyFormatter = useCurrencyFormatter();
const { queryResult } = useShow<IFilament>({
liveMode: "auto",
});
@@ -88,19 +88,12 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
<Title level={5}>{t("filament.fields.name")}</Title>
<TextField value={record?.name} />
<Title level={5}>{t("filament.fields.color_hex")}</Title>
{colorObj && <SpoolIcon color={colorObj} size="large" />}
{colorObj && <SpoolIcon color={colorObj} size="large" no_margin />}
{record?.color_hex && <TextField value={`#${record?.color_hex}`} />}
<Title level={5}>{t("filament.fields.material")}</Title>
<TextField value={record?.material} />
<Title level={5}>{t("filament.fields.price")}</Title>
<NumberField
value={record?.price ?? ""}
options={{
style: "currency",
currency: currency,
currencyDisplay: "narrowSymbol",
notation: "compact",
}}
/>
<TextField value={record?.price ? currencyFormatter.format(record.price) : ""} />
<Title level={5}>{t("filament.fields.density")}</Title>
<NumberFieldUnit
value={record?.density ?? ""}

View File

@@ -7,7 +7,7 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { Trans } from "react-i18next";
import { Link } from "react-router-dom";
import { Link } from "react-router";
dayjs.extend(utc);

View File

@@ -6,7 +6,7 @@ import Title from "antd/es/typography/Title";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import Logo from "../../icon.svg?react";
import { ISpool } from "../spools/model";

View File

@@ -8,7 +8,7 @@ import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import utc from "dayjs/plugin/utc";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import SpoolIcon from "../../../components/spoolIcon";
import { formatWeight } from "../../../utils/parsing";
import { ISpool } from "../../spools/model";

View File

@@ -5,7 +5,7 @@ import { Content } from "antd/es/layout/layout";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router";
import SpoolQRCodePrintingDialog from "./spoolQrCodePrintingDialog";
import SpoolSelectModal from "./spoolSelectModal";

View File

@@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { v4 as uuidv4 } from "uuid";
import { useGetSetting, useSetSetting } from "../../utils/querySettings";
import { ISpool } from "../spools/model";
@@ -75,7 +76,7 @@ function getTagValue(tag: string, obj: GenericObject): any {
return value;
}
function applyNewline(text: string): JSX.Element[] {
function applyNewline(text: string): ReactElement[] {
return text.split("\n").map((line, idx, arr) => (
<span key={idx}>
{line}
@@ -84,7 +85,7 @@ function applyNewline(text: string): JSX.Element[] {
));
}
function applyTextFormatting(text: string): JSX.Element[] {
function applyTextFormatting(text: string): ReactElement[] {
const regex = /\*\*([\w\W]*?)\*\*/g;
const parts = text.split(regex);
// Map over the parts and wrap matched text with <b> tags
@@ -96,7 +97,7 @@ function applyTextFormatting(text: string): JSX.Element[] {
return elements;
}
export function renderLabelContents(template: string, spool: ISpool): JSX.Element {
export function renderLabelContents(template: string, spool: ISpool): ReactElement {
// Find all {tags} in the template string and loop over them
// let matches = [...template.matchAll(/(?:{(.*?))?{(.*?)}(.*?)(?:}(.*?))?/gs)];
let matches = [...template.matchAll(/{(?:[^}{]|{[^}{]*})*}/gs)];

View File

@@ -15,19 +15,19 @@ import {
Space,
} from "antd";
import * as htmlToImage from "html-to-image";
import React, { useRef } from "react";
import ReactToPrint from "react-to-print";
import React, { ReactElement, useRef } from "react";
import { useReactToPrint } from "react-to-print";
import { useSavedState } from "../../utils/saveload";
import { PrintSettings } from "./printing";
interface PrintingDialogProps {
items: JSX.Element[];
items: ReactElement[];
printSettings: PrintSettings;
setPrintSettings: (setPrintSettings: PrintSettings) => void;
style?: string;
extraSettings?: JSX.Element;
extraSettingsStart?: JSX.Element;
extraButtons?: JSX.Element;
extraSettings?: ReactElement;
extraSettingsStart?: ReactElement;
extraButtons?: ReactElement;
}
interface PaperDimensions {
@@ -90,7 +90,8 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({
const paperWidth = paperSize === "custom" ? customPaperSize.width : paperDimensions[paperSize].width;
const paperHeight = paperSize === "custom" ? customPaperSize.height : paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({ contentRef });
const itemWidth = (paperWidth - margin.left - margin.right - spacing.horizontal) / paperColumns - spacing.horizontal;
const itemHeight = (paperHeight - margin.top - margin.bottom - spacing.vertical) / paperRows - spacing.vertical;
@@ -223,7 +224,7 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({
>
<div
className="print-container"
ref={printRef}
ref={contentRef}
style={{
transform: `scale(${previewScale})`,
transformOrigin: "top left",
@@ -817,15 +818,9 @@ const PrintingDialog: React.FC<PrintingDialogProps> = ({
<Button type="primary" icon={<FileImageOutlined />} size="large" onClick={saveAsImage}>
{t("printing.generic.saveAsImage")}
</Button>
<ReactToPrint
key="print-button"
trigger={() => (
<Button type="primary" icon={<PrinterOutlined />} size="large">
{t("printing.generic.print")}
</Button>
)}
content={() => printRef.current}
/>
<Button type="primary" icon={<PrinterOutlined />} size="large" onClick={() => reactToPrintFn()}>
{t("printing.generic.print")}
</Button>
</Space>
</Col>
</Row>

View File

@@ -1,5 +1,7 @@
import { useTranslate } from "@refinedev/core";
import { Col, Form, InputNumber, QRCode, Radio, RadioChangeEvent, Row, Slider, Switch, Typography } from "antd";
import { ReactElement } from "react";
import { getBasePath } from "../../utils/url";
import { QRCodePrintSettings } from "./printing";
import PrintingDialog from "./printingDialog";
@@ -7,7 +9,7 @@ const { Text } = Typography;
interface QRCodeData {
value: string;
label?: JSX.Element;
label?: ReactElement;
errorLevel?: "L" | "M" | "Q" | "H";
}
@@ -15,9 +17,9 @@ interface QRCodePrintingDialogProps {
items: QRCodeData[];
printSettings: QRCodePrintSettings;
setPrintSettings: (setPrintSettings: QRCodePrintSettings) => void;
extraSettings?: JSX.Element;
extraSettingsStart?: JSX.Element;
extraButtons?: JSX.Element;
extraSettings?: ReactElement;
extraSettingsStart?: ReactElement;
extraButtons?: ReactElement;
baseUrlRoot: string;
useHTTPUrl: boolean;
setUseHTTPUrl: (value: boolean) => void;
@@ -47,7 +49,7 @@ const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showQRCodeMode === "withIcon" ? "/favicon.svg" : undefined}
icon={showQRCodeMode === "withIcon" ? getBasePath() + "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"

View File

@@ -3,7 +3,7 @@ import { useTable } from "@refinedev/antd";
import { Button, Checkbox, Col, message, Row, Space, Table } from "antd";
import { t } from "i18next";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "../../components/column";
import { useSpoolmanFilamentFilter, useSpoolmanMaterials } from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";

View File

@@ -22,7 +22,7 @@ import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { useState } from "react";
import { Trans } from "react-i18next";
import { useParams } from "react-router-dom";
import { useParams } from "react-router";
import { DateTimePicker } from "../../components/dateTimePicker";
import { InputNumberRange } from "../../components/inputNumberRange";
import { EntityType, Field, FieldType, useDeleteField, useGetFields, useSetField } from "../../utils/queryFields";
@@ -488,10 +488,16 @@ export function ExtraFieldsSettings() {
} else if (Array.isArray(val) && record.field.field_type === FieldType.choice) {
return val.join(", ");
} else if (
(Array.isArray(val) && record.field.field_type === FieldType.integer_range) ||
record.field.field_type === FieldType.float_range
Array.isArray(val) && (
record.field.field_type === FieldType.integer_range || record.field.field_type === FieldType.float_range
)
) {
return `${val[0]} \u2013 ${val[1]}`;
let lower = val[0] ?? "";
let upper = val[1] ?? "";
if (lower === "" && upper === "") {
return null;
}
return `${lower} \u2013 ${upper}`;
} else {
return null;
}

View File

@@ -1,5 +1,5 @@
import { useTranslate } from "@refinedev/core";
import { Button, Form, Input, message } from "antd";
import { Button, Checkbox, Form, Input, message } from "antd";
import { useEffect } from "react";
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
@@ -7,6 +7,7 @@ export function GeneralSettings() {
const settings = useGetSettings();
const setBaseUrl = useSetSetting("base_url");
const setCurrency = useSetSetting("currency");
const setRoundPrices = useSetSetting("round_prices");
const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const t = useTranslate();
@@ -17,6 +18,7 @@ export function GeneralSettings() {
form.setFieldsValue({
currency: JSON.parse(settings.data.currency.value),
base_url: JSON.parse(settings.data.base_url.value),
round_prices: JSON.parse(settings.data.round_prices.value),
});
}
}, [settings.data, form]);
@@ -29,7 +31,7 @@ export function GeneralSettings() {
}, [setCurrency.isSuccess, messageApi, t]);
// Handle form submit
const onFinish = (values: { currency: string; base_url: string }) => {
const onFinish = (values: { currency: string; base_url: string, round_prices: boolean }) => {
// Check if the currency has changed
if (settings.data?.currency.value !== JSON.stringify(values.currency)) {
setCurrency.mutate(values.currency);
@@ -38,61 +40,74 @@ export function GeneralSettings() {
if (settings.data?.base_url.value !== JSON.stringify(values.base_url)) {
setBaseUrl.mutate(values.base_url);
}
// Check if the setting to round prices has changed
if (settings.data?.round_prices.value !== JSON.stringify(values.round_prices)) {
setRoundPrices.mutate(values.round_prices);
}
};
return (
<>
<Form
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={{
currency: settings.data?.currency.value,
}}
onFinish={onFinish}
style={{
maxWidth: "600px",
margin: "0 auto",
}}
return (<>
<Form
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={{
currency: settings.data?.currency.value,
round_prices: settings.data?.round_prices.value,
}}
onFinish={onFinish}
style={{
maxWidth: "600px",
margin: "0 auto",
}}
>
<Form.Item
label={t("settings.general.currency.label")}
name="currency"
rules={[
{
required: true,
},
{
pattern: /^[A-Z]{3}$/,
},
]}
>
<Form.Item
label={t("settings.general.currency.label")}
name="currency"
rules={[
{
required: true,
},
{
pattern: /^[A-Z]{3}$/,
},
]}
>
<Input />
</Form.Item>
<Input />
</Form.Item>
<Form.Item
label={t("settings.general.base_url.label")}
tooltip={t("settings.general.base_url.tooltip")}
name="base_url"
rules={[
{
required: false,
},
{
pattern: /^https?:\/\/.+(?<!\/)$/,
},
]}
>
<Input placeholder="https://example.com:8000" />
</Form.Item>
<Form.Item
label={t("settings.general.base_url.label")}
tooltip={t("settings.general.base_url.tooltip")}
name="base_url"
rules={[
{
required: false,
},
{
pattern: /^https?:\/\/.+(?<!\/)$/,
},
]}
>
<Input placeholder="https://example.com:8000" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isLoading}>
{t("buttons.save")}
</Button>
</Form.Item>
</Form>
{contextHolder}
</>
);
<Form.Item
label={t("settings.general.round_prices.label")}
tooltip={t("settings.general.round_prices.tooltip")}
name="round_prices"
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isLoading}>
{t("buttons.save")}
</Button>
</Form.Item>
</Form>
{contextHolder}
</>);
}

View File

@@ -5,7 +5,7 @@ import { Content } from "antd/es/layout/layout";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { Route, Routes, useNavigate } from "react-router-dom";
import { Route, Routes, useNavigate } from "react-router";
import { ExtraFieldsSettings } from "./extraFieldsSettings";
import { GeneralSettings } from "./generalSettings";

View File

@@ -10,7 +10,7 @@ import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../compo
import { useSpoolmanLocations } from "../../components/otherModels";
import { searchMatches } from "../../utils/filtering";
import "../../utils/overrides.css";
import { numberFormatter, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { createFilamentFromExternal } from "../filaments/functions";
@@ -330,7 +330,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
<InputNumber
addonAfter={getCurrencySymbol(undefined, currency)}
precision={2}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParserAllowEmpty}
/>
</Form.Item>
@@ -391,7 +391,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.used_weight}
value={usedWeight}
@@ -409,7 +409,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.remaining_weight}
value={getRemainingWeight()}
@@ -427,7 +427,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.measured_weight}
value={getMeasuredWeight()}

View File

@@ -5,11 +5,11 @@ import TextArea from "antd/es/input/TextArea";
import { message } from "antd/lib";
import dayjs from "dayjs";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { useSpoolmanLocations } from "../../components/otherModels";
import { searchMatches } from "../../utils/filtering";
import { numberFormatter, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { formatNumberOnUserInput, numberParser, numberParserAllowEmpty } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { createFilamentFromExternal } from "../filaments/functions";
@@ -320,7 +320,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
<InputNumber
addonAfter={getCurrencySymbol(undefined, currency)}
precision={2}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParserAllowEmpty}
/>
</Form.Item>
@@ -380,7 +380,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.used_weight}
value={usedWeight}
@@ -394,7 +394,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.remaining_weight}
value={getRemainingWeight()}
@@ -408,7 +408,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
min={0}
addonAfter="g"
precision={1}
formatter={numberFormatter}
formatter={formatNumberOnUserInput}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.measured_weight}
value={getMeasuredWeight()}

View File

@@ -2,7 +2,7 @@ import { useSelect, useTranslate } from "@refinedev/core";
import { useQueries } from "@tanstack/react-query";
import { Form, InputNumber, Modal, Radio } from "antd";
import { useForm } from "antd/es/form/Form";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { formatLength, formatWeight } from "../../utils/parsing";
import { SpoolType, useGetExternalDBFilaments } from "../../utils/queryExternalDB";
import { getAPIURL } from "../../utils/url";
@@ -181,9 +181,13 @@ export function useSpoolAdjustModal() {
const [curSpool, setCurSpool] = useState<ISpool | null>(null);
const [measurementType, setMeasurementType] = useState<MeasurementType>("length");
const inputNumberRef = useRef<HTMLInputElement | null>(null);
const openSpoolAdjustModal = useCallback((spool: ISpool) => {
setCurSpool(spool);
setTimeout(() => {
inputNumberRef.current?.focus();
}, 0);
}, []);
const spoolAdjustModal = useMemo(() => {
@@ -224,7 +228,7 @@ export function useSpoolAdjustModal() {
</Radio.Group>
</Form.Item>
<Form.Item label={t("spool.form.adjust_filament_value")} name="filament_value">
<InputNumber precision={1} addonAfter={measurementType === "length" ? "mm" : "g"} />
<InputNumber ref={inputNumberRef} precision={1} addonAfter={measurementType === "length" ? "mm" : "g"} />
</Form.Item>
</Form>
</Modal>

View File

@@ -1,12 +1,12 @@
import {
EditOutlined,
EyeOutlined,
FilterOutlined,
InboxOutlined,
PlusSquareOutlined,
PrinterOutlined,
ToolOutlined,
ToTopOutlined,
EditOutlined,
EyeOutlined,
FilterOutlined,
InboxOutlined,
PlusSquareOutlined,
PrinterOutlined,
ToolOutlined,
ToTopOutlined,
} from "@ant-design/icons";
import { List, useTable } from "@refinedev/antd";
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
@@ -14,29 +14,29 @@ import { Button, Dropdown, Modal, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useCallback, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import {
Action,
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
Action,
ActionsColumn,
CustomFieldColumn,
DateColumn,
FilteredQueryColumn,
NumberColumn,
RichColumn,
SortedColumn,
SpoolIconColumn,
} from "../../components/column";
import { useLiveify } from "../../components/liveify";
import {
useSpoolmanFilamentFilter,
useSpoolmanLocations,
useSpoolmanLotNumbers,
useSpoolmanMaterials,
useSpoolmanFilamentFilter,
useSpoolmanLocations,
useSpoolmanLotNumbers,
useSpoolmanMaterials,
} from "../../components/otherModels";
import { removeUndefined } from "../../utils/filtering";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
import { useCurrency } from "../../utils/settings";
import { useCurrencyFormatter } from "../../utils/settings";
import { setSpoolArchived, useSpoolAdjustModal } from "./functions";
import { ISpool } from "./model";
@@ -102,7 +102,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
const invalidate = useInvalidate();
const navigate = useNavigate();
const extraFields = useGetFields(EntityType.spool);
const currency = useCurrency();
const currencyFormatter = useCurrencyFormatter();
const { openSpoolAdjustModal, spoolAdjustModal } = useSpoolAdjustModal();
const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
@@ -381,12 +381,10 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
align: "right",
width: 80,
render: (_, obj: ISpoolCollapsed) => {
return obj.price?.toLocaleString(undefined, {
style: "currency",
currencyDisplay: "narrowSymbol",
currency: currency,
notation: "compact",
});
if (obj.price === undefined) {
return "";
}
return currencyFormatter.format(obj.price);
},
}),
NumberColumn({

View File

@@ -1,3 +1,4 @@
import { getBasePath } from "../../utils/url";
import { InboxOutlined, PrinterOutlined, ToTopOutlined, ToolOutlined } from "@ant-design/icons";
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import { IResourceComponentsProps, useInvalidate, useShow, useTranslate } from "@refinedev/core";
@@ -10,7 +11,7 @@ import { NumberFieldUnit } from "../../components/numberField";
import SpoolIcon from "../../components/spoolIcon";
import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrency } from "../../utils/settings";
import { useCurrencyFormatter } from "../../utils/settings";
import { IFilament } from "../filaments/model";
import { setSpoolArchived, useSpoolAdjustModal } from "./functions";
import { ISpool } from "./model";
@@ -23,7 +24,7 @@ const { confirm } = Modal;
export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const extraFields = useGetFields(EntityType.spool);
const currency = useCurrency();
const currencyFormatter = useCurrencyFormatter();
const invalidate = useInvalidate();
const { queryResult } = useShow<ISpool>({
@@ -33,13 +34,12 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
const record = data?.data;
const spoolPrice = (item: ISpool) => {
let spoolPrice = "";
if (item.price === undefined) {
spoolPrice = `${item.filament.price}`;
return spoolPrice;
const spoolPrice = (item?: ISpool) => {
const price = item?.price ?? item?.filament.price;
if (price === undefined) {
return "";
}
return item.price;
return currencyFormatter.format(price);
};
// Provides the function to open the spool adjustment modal and the modal component itself
@@ -128,7 +128,7 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
<Button
type="primary"
icon={<PrinterOutlined />}
href={"/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
href={getBasePath() + "/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
>
{t("printing.qrcode.button")}
</Button>
@@ -150,17 +150,10 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
<Title level={5}>{t("spool.fields.id")}</Title>
<NumberField value={record?.id ?? ""} />
<Title level={5}>{t("spool.fields.filament")}</Title>
{colorObj && <SpoolIcon color={colorObj} />} <TextField value={record ? filamentURL(record?.filament) : ""} />
{colorObj && <SpoolIcon color={colorObj} size="large" no_margin />}
<TextField value={record ? filamentURL(record?.filament) : ""} />
<Title level={5}>{t("spool.fields.price")}</Title>
<NumberField
value={record ? spoolPrice(record) : ""}
options={{
style: "currency",
currency: currency,
currencyDisplay: "narrowSymbol",
notation: "compact",
}}
/>
<TextField value={spoolPrice(record)} />
<Title level={5}>{t("spool.fields.registered")}</Title>
<DateField
value={dayjs.utc(record?.registered).local()}

View File

@@ -5,14 +5,14 @@ import { Button, Dropdown, Table } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { useCallback, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import {
ActionsColumn,
CustomFieldColumn,
DateColumn,
NumberColumn,
RichColumn,
SortedColumn,
ActionsColumn,
CustomFieldColumn,
DateColumn,
NumberColumn,
RichColumn,
SortedColumn,
} from "../../components/column";
import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";

View File

@@ -24,6 +24,22 @@ export function formatNumberWithSpaceSeparator(input: string): string {
return formattedNumber;
}
/**
* Number formatter compatible with Ant Design's InputNumber component, handling UX properly.
* @param value
* @param info
* @returns
*/
export function formatNumberOnUserInput(
value: number | string | undefined,
info: { userTyping: boolean; input: string }
): string {
if (info.userTyping) {
return info.input;
}
return numberFormatter(value);
}
/**
* Number formatter that nicely formats numbers with correct decimal separator based on locale
* Always uses blank space as thousands separator to prevent confusion with the decimal separator
@@ -31,7 +47,6 @@ export function formatNumberWithSpaceSeparator(input: string): string {
* @returns
*/
export function numberFormatter(value: number | string | undefined): string {
console.log("numberformatter input: ", value);
const formattedValue = value
? Number(value).toLocaleString(undefined, {
useGrouping: false, // Disable thousands separator and do it manually instead so it's always spaces
@@ -117,10 +132,10 @@ export function enrichText(text: string | undefined) {
*/
export function formatWeight(weightInGrams: number, precision: number = 2): string {
if (weightInGrams >= 1000) {
const kilograms = (weightInGrams / 1000).toFixed(precision).replace(/\.?0+$/, ""); // Remove trailing zeros
const kilograms = removeTrailingZeros((weightInGrams / 1000).toFixed(precision));
return `${kilograms} kg`;
} else {
const grams = weightInGrams.toFixed(precision).replace(/\.?0+$/, ""); // Remove trailing zeros
const grams = removeTrailingZeros(weightInGrams.toFixed(precision));
return `${grams} g`;
}
}
@@ -134,9 +149,30 @@ export function formatWeight(weightInGrams: number, precision: number = 2): stri
*/
export function formatLength(lengthInMillimeter: number, precision: number = 2): string {
if (lengthInMillimeter >= 1000) {
const meters = (lengthInMillimeter / 1000).toFixed(precision).replace(/\.?0+$/, ""); // Remove trailing zeros
const meters = removeTrailingZeros((lengthInMillimeter / 1000).toFixed(precision));
return `${meters} m`;
} else {
return `${lengthInMillimeter} mm`;
}
}
/**
* Removes trailing zeros from a numeric string, including unnecessary decimal points.
*
* This function takes a string representation of a number and removes any trailing zeros
* after the decimal point. If the number ends with a decimal point followed by only zeros,
* the entire decimal portion is removed.
*
* @param num - The numeric string to process.
* @returns The numeric string with trailing zeros removed.
*
* @example
* ```typescript
* removeTrailingZeros("123.45000"); // Returns "123.45"
* removeTrailingZeros("100.000"); // Returns "100"
* removeTrailingZeros("0.0000"); // Returns "0"
* ```
*/
function removeTrailingZeros(num: string): string {
return num.replace(/(\.\d*?[1-9])0+|\.0*$/, "$1");
}

View File

@@ -17,3 +17,15 @@ export function getCurrencySymbol(locale: string | undefined, currency: string)
.replace(/\d/g, "")
.trim();
}
export function useCurrencyFormatter() {
const currency = useCurrency();
const roundPrices = JSON.parse(useGetSetting("round_prices").data?.value ?? "false");
return new Intl.NumberFormat(undefined, {
style: "currency",
currency: currency,
currencyDisplay: "narrowSymbol",
notation: roundPrices ? "compact" : "standard",
});
}