Merge remote-tracking branch 'upstream/master'

This commit is contained in:
samturner3
2024-08-24 16:18:17 +10:00
159 changed files with 9157 additions and 6235 deletions

View File

@@ -1,19 +1,18 @@
import { DateField, TextField } from "@refinedev/antd";
import { UseQueryResult } from "@tanstack/react-query";
import { Button, Col, Dropdown, Row, Space, Spin } from "antd";
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 { getFiltersForField, typeFilters } from "../utils/filtering";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { TableState } from "../utils/saveload";
import { getSortOrderForField, typeSorters } from "../utils/sorting";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
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 { enrichText } from "../utils/parsing";
import { UseQueryResult } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { Field, FieldType } from "../utils/queryFields";
import SpoolIcon from "./spoolIcon";
dayjs.extend(utc);
@@ -45,6 +44,7 @@ interface BaseColumnProps<Obj extends Entity> {
i18ncat?: string;
i18nkey?: string;
title?: string;
align?: AlignType;
sorter?: boolean;
t: (key: string) => string;
navigate: (link: string) => void;
@@ -88,6 +88,7 @@ function Column<Obj extends Entity>(
const columnProps: ColumnType<Obj> = {
dataIndex: props.id,
align: props.align,
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
filterMultiple: props.allowMultipleFilters ?? true,
width: props.width ?? undefined,
@@ -196,7 +197,7 @@ export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColu
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -227,6 +228,7 @@ interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
return Column({
...props,
align: "right",
render: (rawValue) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
if (value === null || value === undefined) {
@@ -256,18 +258,19 @@ export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
hidden={!value}
value={dayjs.utc(value).local()}
title={dayjs.utc(value).local().format()}
format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
/>
);
},
});
}
export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Action[]): ColumnType<Obj> | undefined {
const t = useTranslate();
export function ActionsColumn<Obj extends Entity>(
title: string,
actionsFn: (record: Obj) => Action[]
): ColumnType<Obj> | undefined {
return {
title: t("table.actions"),
title,
responsive: ["lg"],
render: (_, record) => {
const buttons = actionsFn(record).map((action) => {
@@ -296,7 +299,7 @@ export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Ac
}
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>) {
@@ -308,7 +311,7 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
if (typeof item === "string") {
return {
text: item,
value: item,
value: '"' + item + '"',
};
}
return item;
@@ -343,19 +346,12 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
},
render: (rawValue, record: Obj) => {
const value = props.transform ? props.transform(rawValue) : rawValue;
const colorStr = props.color(record);
const colorObj = props.color(record);
return (
<Row wrap={false} justify="space-around" align="middle">
{colorStr && (
{colorObj && (
<Col flex="none">
<Icon
component={SpoolIcon}
style={{
color: "#" + colorStr,
fontSize: 42,
marginRight: 0,
}}
/>
<SpoolIcon color={colorObj} />
</Col>
)}
<Col flex="auto">{value}</Col>

View File

@@ -1,7 +1,7 @@
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
import { DataProvider } from "@refinedev/core";
import { axiosInstance } from "@refinedev/simple-rest";
import { AxiosInstance } from "axios";
import { stringify } from "query-string";
type MethodTypes = "get" | "delete" | "head" | "options";
type MethodTypesWithBody = "post" | "put" | "patch";

View File

@@ -1,8 +1,8 @@
import { DatePicker } from "antd";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import advancedFormat from "dayjs/plugin/advancedFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.extend(timezone);

View File

@@ -1,13 +1,13 @@
import { DateField, TextField } from "@refinedev/antd";
import { Field, FieldType } from "../utils/queryFields";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
import { enrichText } from "../utils/parsing";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Checkbox, Form, Input, InputNumber, Select, Typography } from "antd";
import { FormItemProps, Rule } from "antd/es/form";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { enrichText } from "../utils/parsing";
import { Field, FieldType } from "../utils/queryFields";
import { DateTimePicker } from "./dateTimePicker";
import { InputNumberRange } from "./inputNumberRange";
import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField";
dayjs.extend(utc);

View File

@@ -0,0 +1,20 @@
import { useEffect } from "react";
/**
* Renders a favicon element in the head of the HTML document with the specified URL.
*
* @param {string} props.url - The URL of the favicon image.
* @return {JSX.Element} - An empty JSX element.
*/
export function Favicon(props: { url: string }) {
useEffect(() => {
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!link) {
link = document.createElement("link") as HTMLLinkElement;
link.rel = "icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
link.href = props.url;
}, [props.url]);
return <></>;
}

View File

@@ -0,0 +1,72 @@
import { useTranslate } from "@refinedev/core";
import { Form, Modal, Select } from "antd";
import { Trans } from "react-i18next";
import { formatFilamentLabel } from "../pages/spools/functions";
import { searchMatches } from "../utils/filtering";
import { ExternalFilament, useGetExternalDBFilaments } from "../utils/queryExternalDB";
export function FilamentImportModal(props: {
isOpen: boolean;
onImport: (filament: ExternalFilament) => void;
onClose: () => void;
}) {
const [form] = Form.useForm();
const t = useTranslate();
const externalFilaments = useGetExternalDBFilaments();
const filamentOptions =
externalFilaments.data?.map((item) => {
return {
label: formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type
),
value: item.id,
item: item,
};
}) ?? [];
filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
return (
<Modal
title={t("filament.form.import_external")}
open={props.isOpen}
onOk={() => form.submit()}
onCancel={() => props.onClose()}
>
<Form
layout="vertical"
form={form}
onFinish={(values) => {
const filament = filamentOptions.find((item) => item.value === values.filament)?.item;
if (!filament) {
throw new Error("Filament not found");
}
props.onImport(filament);
props.onClose();
form.resetFields();
}}
>
<p>
<Trans
i18nKey={"filament.form.import_external_description"}
components={{
br: <br />,
}}
/>
</p>
<Form.Item name="filament" rules={[{ required: true }]}>
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
/>
</Form.Item>
</Form>
</Modal>
);
}

View File

@@ -1,13 +1,13 @@
import { DownOutlined } from "@ant-design/icons";
import type { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd";
import { useGetLocale, useSetLocale } from "@refinedev/core";
import { Button, Dropdown, Layout as AntdLayout, MenuProps, Space, Switch, theme } from "antd";
import { Layout as AntdLayout, Button, Dropdown, MenuProps, Space, Switch, theme } from "antd";
import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
import "/node_modules/flag-icons/css/flag-icons.min.css";
const { useToken } = theme;

View File

@@ -1,10 +1,11 @@
import { ThemedLayoutV2, ThemedSiderV2, ThemedTitleV2 } from "@refinedev/antd";
import { Header } from "./header";
import { Footer } from "antd/es/layout/layout";
import { Version } from "./version";
import { Button } from "antd";
import Logo from "../icon.svg?react";
import { useTranslate } from "@refinedev/core";
import { Button } from "antd";
import { Footer } from "antd/es/layout/layout";
import Logo from "../icon.svg?react";
import { getBasePath } from "../utils/url";
import { Header } from "./header";
import { Version } from "./version";
const SpoolmanFooter: React.FC = () => {
const t = useTranslate();
@@ -27,7 +28,7 @@ const SpoolmanFooter: React.FC = () => {
<Button
icon={
<img
src="/kofi_s_logo_nolabel.png"
src={getBasePath() + "/kofi_s_logo_nolabel.png"}
style={{
height: "1.6em",
}}

View File

@@ -1,8 +1,9 @@
import { LiveEvent } from "@refinedev/core";
import React from "react";
import { useEffect, useState } from "react";
import { getAPIURL } from "../utils/url";
import liveProvider from "./liveProvider";
const liveProviderInstance = liveProvider(import.meta.env.VITE_APIURL);
const liveProviderInstance = liveProvider(getAPIURL());
/**
* Hook that subscribes to live updates for the items in the dataSource
@@ -15,29 +16,23 @@ export function useLiveify<Data extends { id: number }>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transformPayload: (payload: any) => Data
) {
// TODO: The hooks in this function is quite janky, and should be refactored to be more efficient
// New state that holds the dataSource with updated values from the live provider
const [updatedDataSource, setUpdatedDataSource] = React.useState<Data[]>(dataSource);
const [updatedDataSource, setUpdatedDataSource] = useState<Data[]>(dataSource);
// If the original dataSource changes, update the updatedDataSource
React.useEffect(() => {
useEffect(() => {
setUpdatedDataSource(dataSource);
}, [dataSource]);
// Create a constant reference to itemIds. This is to prevent the useEffect below from triggering extra times.
const itemIds = dataSource.map((item) => item.id);
const [prevItemIds, setPrevItemIds] = React.useState<number[]>(itemIds);
if (JSON.stringify(itemIds) !== JSON.stringify(prevItemIds)) {
setPrevItemIds(itemIds);
}
// Subscribe to changes for all items in the dataSource
React.useEffect(() => {
useEffect(() => {
const itemIds = dataSource.map((item) => item.id);
const subscription = liveProviderInstance?.subscribe({
channel: `${resource}-list`,
params: {
resource: resource,
ids: prevItemIds,
ids: itemIds,
subscriptionType: "useList",
},
types: ["update"],
@@ -56,7 +51,7 @@ export function useLiveify<Data extends { id: number }>(
liveProviderInstance?.unsubscribe(subscription);
}
};
}, [resource, prevItemIds, transformPayload]);
}, [resource, dataSource, transformPayload]);
return updatedDataSource;
}

View File

@@ -0,0 +1,83 @@
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
import { Badge, Button, ColorPicker, 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
value={value}
onChange={(clr) => {
if (props.onChange) {
props.onChange(values.map((v, i) => (i === idx ? clr.toHex() : 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>
</>
);
}

View File

@@ -1,7 +1,40 @@
import { NotificationProvider } from "@refinedev/core";
import { message } from "antd";
import { UndoOutlined } from "@ant-design/icons";
import { NotificationProvider, OpenNotificationParams } from "@refinedev/core";
import { Button, Progress, message } from "antd";
import { UndoableNotification } from "@refinedev/antd/src/components/undoableNotification";
type UndoableNotificationProps = {
notificationKey: OpenNotificationParams["key"];
message: OpenNotificationParams["message"];
cancelMutation: OpenNotificationParams["cancelMutation"];
undoableTimeout: OpenNotificationParams["undoableTimeout"];
};
const UndoableNotification: React.FC<UndoableNotificationProps> = ({ message, cancelMutation, undoableTimeout }) => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: "-7px",
}}
>
<Progress
type="circle"
percent={(undoableTimeout ?? 0) * 20}
format={(time) => time && time / 20}
size={50}
strokeColor="#1890ff"
status="normal"
/>
<span style={{ marginLeft: 8, width: "100%" }}>{message}</span>
<Button
style={{ flexShrink: 0 }}
onClick={cancelMutation}
disabled={undoableTimeout === 0}
icon={<UndoOutlined />}
></Button>
</div>
);
const SpoolmanNotificationProvider: NotificationProvider = {
open: ({ key, message: content, type, cancelMutation, undoableTimeout }) => {

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Typography } from "antd";
import { NumberFieldProps } from "@refinedev/antd/dist/components/fields/types";
import { Typography } from "antd";
import React from "react";
const { Text } = Typography;

View File

@@ -1,16 +1,16 @@
import { useQuery } from "@tanstack/react-query";
import { Tooltip } from "antd";
import { ColumnFilterItem } from "antd/es/table/interface";
import { IFilament } from "../pages/filaments/model";
import { IVendor } from "../pages/vendors/model";
import { ColumnFilterItem } from "antd/es/table/interface";
import { Tooltip } from "antd";
import { getAPIURL } from "../utils/url";
export function useSpoolmanFilamentFilter(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, ColumnFilterItem[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -86,12 +86,11 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
}
export function useSpoolmanFilamentNames(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IFilament[], unknown, string[]>({
enabled: enabled,
queryKey: ["filaments"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/filament");
const response = await fetch(getAPIURL() + "/filament");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -115,12 +114,11 @@ export function useSpoolmanFilamentNames(enabled: boolean = false) {
}
export function useSpoolmanVendors(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<IVendor[], unknown, string[]>({
enabled: enabled,
queryKey: ["vendors"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/vendor");
const response = await fetch(getAPIURL() + "/vendor");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -137,12 +135,11 @@ export function useSpoolmanVendors(enabled: boolean = false) {
}
export function useSpoolmanMaterials(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["materials"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/material");
const response = await fetch(getAPIURL() + "/material");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -155,12 +152,11 @@ export function useSpoolmanMaterials(enabled: boolean = false) {
}
export function useSpoolmanArticleNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["articleNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/article-number");
const response = await fetch(getAPIURL() + "/article-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -173,12 +169,11 @@ export function useSpoolmanArticleNumbers(enabled: boolean = false) {
}
export function useSpoolmanLotNumbers(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["lotNumbers"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/lot-number");
const response = await fetch(getAPIURL() + "/lot-number");
if (!response.ok) {
throw new Error("Network response was not ok");
}
@@ -191,12 +186,11 @@ export function useSpoolmanLotNumbers(enabled: boolean = false) {
}
export function useSpoolmanLocations(enabled: boolean = false) {
const apiEndpoint = import.meta.env.VITE_APIURL;
return useQuery<string[]>({
enabled: enabled,
queryKey: ["locations"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/location");
const response = await fetch(getAPIURL() + "/location");
if (!response.ok) {
throw new Error("Network response was not ok");
}

View File

@@ -1,700 +0,0 @@
import React, { useRef } from "react";
import {
Modal,
Slider,
Button,
Select,
Row,
Col,
Form,
Divider,
RadioChangeEvent,
Radio,
InputNumber,
Collapse,
} from "antd";
import ReactToPrint from "react-to-print";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
interface PrintingDialogProps {
items: JSX.Element[];
style?: string;
extraSettings?: JSX.Element;
visible: boolean;
onCancel: () => void;
title?: string;
}
interface PaperDimensions {
width: number;
height: number;
}
const paperDimensions: { [key: string]: PaperDimensions } = {
A3: {
width: 297,
height: 420,
},
A4: {
width: 210,
height: 297,
},
A5: {
width: 148,
height: 210,
},
Letter: {
width: 216,
height: 279,
},
Legal: {
width: 216,
height: 356,
},
Tabloid: {
width: 279,
height: 432,
},
};
const PrintingDialog: React.FC<PrintingDialogProps> = ({ items, style, extraSettings, visible, onCancel, title }) => {
const t = useTranslate();
const [collapseState, setCollapseState] = useSavedState<string[]>("print-collapseState", []);
const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10);
const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10);
const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10);
const [marginBottom, setMarginBottom] = useSavedState("print-marginBottom", 10);
const [horizontalSpacing, setHorizontalSpacing] = useSavedState("print-horizontalSpacing", 0);
const [verticalSpacing, setVerticalSpacing] = useSavedState("print-verticalSpacing", 0);
const [printerMarginLeft, setPrinterMarginLeft] = useSavedState("print-printerMarginLeft", 5);
const [printerMarginTop, setPrinterMarginTop] = useSavedState("print-printerMarginTop", 5);
const [printerMarginRight, setPrinterMarginRight] = useSavedState("print-printerMarginRight", 5);
const [printerMarginBottom, setPrinterMarginBottom] = useSavedState("print-printerMarginBottom", 5);
const [paperColumns, setPaperColumns] = useSavedState("print-itemsPerRow", 3);
const [paperRows, setPaperRows] = useSavedState("print-rowsPerPage", 8);
const [skipItems, setSkipItems] = useSavedState("print-skipItems", 0);
const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4");
const [customPaperWidth, setCustomPaperWidth] = useSavedState("print-customPaperWidth", 210);
const [customPaperHeight, setCustomPaperHeight] = useSavedState("print-customPaperHeight", 297);
const [previewScale, setPreviewScale] = useSavedState("print-previewScale", 0.6);
const [borderShowMode, setBorderShowMode] = useSavedState<"none" | "border" | "grid">("print-borderShowMode", "grid");
const paperWidth = paperSize === "custom" ? customPaperWidth : paperDimensions[paperSize].width;
const paperHeight = paperSize === "custom" ? customPaperHeight : paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const itemWidth = (paperWidth - marginLeft - marginRight - horizontalSpacing) / paperColumns - horizontalSpacing;
const itemHeight = (paperHeight - marginTop - marginBottom - verticalSpacing) / paperRows - verticalSpacing;
const itemsPerRow = paperColumns;
const rowsPerPage = paperRows;
const itemsIncludingSkipped = [...Array(skipItems).fill(<></>), ...items];
const rowsOfItems = [];
for (let row_idx = 0; row_idx < itemsIncludingSkipped.length / itemsPerRow; row_idx += 1) {
rowsOfItems.push(itemsIncludingSkipped.slice(row_idx * itemsPerRow, (row_idx + 1) * itemsPerRow));
}
const pageBlocks = [];
for (let page_idx = 0; page_idx < rowsOfItems.length / rowsPerPage; page_idx += 1) {
pageBlocks.push(rowsOfItems.slice(page_idx * rowsPerPage, (page_idx + 1) * rowsPerPage));
}
const pages = pageBlocks.map(function (rows, pageIdx) {
const itemRows = rows.map((row, rowIdx) => {
return (
<tr key={rowIdx}>
{row.map(function (item, colIdx) {
return (
<td>
<div
key={colIdx}
style={{
width: `${itemWidth}mm`,
height: `${itemHeight}mm`,
border: borderShowMode === "grid" ? "1px solid #000" : "none",
paddingLeft: colIdx === 0 ? `${Math.max(printerMarginLeft - marginLeft, 0)}mm` : 0,
paddingRight:
colIdx === paperColumns - 1 ? `${Math.max(printerMarginRight - marginRight, 0)}mm` : 0,
paddingTop: rowIdx === 0 ? `${Math.max(printerMarginTop - marginTop, 0)}mm` : 0,
paddingBottom:
rowIdx === paperRows - 1 ? `${Math.max(printerMarginBottom - marginBottom, 0)}mm` : 0,
}}
>
{item}
</div>
</td>
);
})}
</tr>
);
});
return (
<div
className="print-page"
key={pageIdx}
style={{
width: `${paperWidth}mm`,
height: `${paperHeight}mm`,
backgroundColor: "#FFF",
overflow: "hidden",
}}
>
<div
className="print-page-area"
style={{
border: borderShowMode !== "none" ? "1px solid #000" : "none",
height: `${paperHeight - marginTop - marginBottom}mm`,
width: `${paperWidth - marginLeft - marginRight}mm`,
marginTop: `calc(${marginTop}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginLeft: `calc(${marginLeft}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginRight: `calc(${marginRight}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
marginBottom: `calc(${marginBottom}mm - ${borderShowMode !== "none" ? "1px" : "0px"})`,
}}
>
<table
style={{
alignContent: "flex-start",
}}
>
{itemRows}
</table>
</div>
</div>
);
});
return (
<Modal
open={visible}
title={title ?? t("printing.generic.title")}
onCancel={onCancel}
footer={[
<ReactToPrint
key="print-button"
trigger={() => <Button type="primary">{t("printing.generic.print")}</Button>}
content={() => printRef.current}
/>,
]}
width={1200} // Set the modal width to accommodate the preview
>
<Row gutter={16}>
<Col
span={24}
style={{
whiteSpace: "pre-line",
marginBottom: "1em",
}}
>
{t("printing.generic.description")}
</Col>
<Col span={14}>
<div
style={{
transform: "translateZ(0)",
overflow: "hidden scroll",
height: "70vh",
}}
>
<div
className="print-container"
ref={printRef}
style={{
transform: `scale(${previewScale})`,
transformOrigin: "top left",
display: "block",
}}
>
<style>
{`
@media print {
@page {
size: auto;
margin: 0;
}
.print-container {
transform: scale(1) !important;
}
.print-page {
page-break-before: auto;
}
}
@media screen {
.print-page {
margin-top: 10mm;
}
}
.print-page table {
border-spacing: ${horizontalSpacing}mm ${verticalSpacing}mm;
border-collapse: separate;
}
.print-page td {
padding: 0;
}
${style ?? ""}
`}
</style>
{pages}
</div>
</div>
</Col>
<Col span={10}>
<Form labelAlign="left" colon={false} labelWrap={true} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}>
<Form.Item label={t("printing.generic.skipItems")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={30}
value={skipItems}
onChange={(value) => {
setSkipItems(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={skipItems}
onChange={(value) => {
setSkipItems(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.showBorder")}>
<Radio.Group
options={[
{ label: t("printing.generic.borders.none"), value: "none" },
{
label: t("printing.generic.borders.border"),
value: "border",
},
{ label: t("printing.generic.borders.grid"), value: "grid" },
]}
onChange={(e: RadioChangeEvent) => {
setBorderShowMode(e.target.value);
}}
value={borderShowMode}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t("printing.generic.previewScale")}>
<Row>
<Col span={12}>
<Slider
min={0.1}
max={1}
step={0.01}
value={previewScale}
onChange={(value) => {
setPreviewScale(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0.1}
max={1}
step={0.01}
style={{ margin: "0 16px" }}
value={previewScale}
onChange={(value) => {
setPreviewScale(value ?? 0.1);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<Collapse
defaultActiveKey={collapseState}
bordered={false}
ghost
onChange={(key) => {
if (Array.isArray(key)) {
setCollapseState(key);
}
}}
>
<Collapse.Panel header={t("printing.generic.contentSettings")} key="1">
{extraSettings}
</Collapse.Panel>
<Collapse.Panel header={t("printing.generic.layoutSettings")} key="2">
<Form.Item label={t("printing.generic.paperSize")}>
<Select value={paperSize} onChange={(value) => setPaperSize(value)}>
{Object.keys(paperDimensions).map((key) => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
<Select.Option value="custom">{t("printing.generic.customSize")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label={t("printing.generic.dimensions")} hidden={paperSize !== "custom"}>
<Row align="middle">
<Col span={11}>
<InputNumber
value={customPaperWidth}
min={0.1}
addonAfter="mm"
onChange={(value) => setCustomPaperWidth(value ?? 0)}
/>
</Col>
<Col span={2} style={{ textAlign: "center" }}>
x
</Col>
<Col span={11}>
<InputNumber
value={customPaperHeight}
min={0.1}
addonAfter="mm"
onChange={(value) => setCustomPaperHeight(value ?? 0)}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.columns")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={5}
value={paperColumns}
onChange={(value) => {
setPaperColumns(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperColumns}
onChange={(value) => {
setPaperColumns(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.rows")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={15}
value={paperRows}
onChange={(value) => {
setPaperRows(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperRows}
onChange={(value) => {
setPaperRows(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<p>{t("printing.generic.helpMargin")}</p>
<Form.Item label={t("printing.generic.marginLeft")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginLeft}
onChange={(value) => {
setMarginLeft(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginLeft}
addonAfter="mm"
onChange={(value) => {
setMarginLeft(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginTop")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginTop}
onChange={(value) => {
setMarginTop(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginTop}
addonAfter="mm"
onChange={(value) => {
setMarginTop(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginRight")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginRight}
onChange={(value) => {
setMarginRight(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginRight}
addonAfter="mm"
onChange={(value) => {
setMarginRight(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginBottom")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginBottom}
onChange={(value) => {
setMarginBottom(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginBottom}
addonAfter="mm"
onChange={(value) => {
setMarginBottom(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<p>{t("printing.generic.helpPrinterMargin")}</p>
<Form.Item label={t("printing.generic.printerMarginLeft")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginLeft}
onChange={(value) => {
setPrinterMarginLeft(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginLeft}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginLeft(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginTop")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginTop}
onChange={(value) => {
setPrinterMarginTop(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginTop}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginTop(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginRight")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginRight}
onChange={(value) => {
setPrinterMarginRight(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginRight}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginRight(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.printerMarginBottom")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={printerMarginBottom}
onChange={(value) => {
setPrinterMarginBottom(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={printerMarginBottom}
addonAfter="mm"
onChange={(value) => {
setPrinterMarginBottom(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Divider />
<Form.Item label={t("printing.generic.horizontalSpacing")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={horizontalSpacing}
onChange={(value) => {
setHorizontalSpacing(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={horizontalSpacing}
addonAfter="mm"
onChange={(value) => {
setHorizontalSpacing(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.verticalSpacing")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={20}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={verticalSpacing}
onChange={(value) => {
setVerticalSpacing(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={verticalSpacing}
addonAfter="mm"
onChange={(value) => {
setVerticalSpacing(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
</Collapse.Panel>
</Collapse>
</Form>
</Col>
</Row>
</Modal>
);
};
export default PrintingDialog;

View File

@@ -1,129 +0,0 @@
import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd";
import { useSavedState } from "../../utils/saveload";
import PrintingDialog from "./printingDialog";
import { useTranslate } from "@refinedev/core";
interface QRCodeData {
value: string;
label?: JSX.Element;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
visible: boolean;
items: QRCodeData[];
onCancel: () => void;
extraSettings?: JSX.Element;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({ visible, items, onCancel, extraSettings }) => {
const t = useTranslate();
const [showContent, setShowContent] = useSavedState("print-showContent", true);
const [textSize, setTextSize] = useSavedState("print-textSize", 3);
const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState("print-showSpoolmanIcon", true);
const elements = items.map((item) => {
return (
<div className="print-qrcode-item">
<div className="print-qrcode-container">
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.svg" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
</div>
{showContent && <div className="print-qrcode-title">{item.label ?? item.value}</div>}
</div>
);
});
return (
<PrintingDialog
visible={visible}
title={t("printing.qrcode.title")}
items={elements}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch checked={showContent} onChange={(checked) => setShowContent(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={2}
max={7}
value={textSize}
step={0.1}
onChange={(value) => {
setTextSize(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
setTextSize(value ?? 5);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolmanIcon")}>
<Switch checked={showSpoolmanIcon} onChange={(checked) => setShowSpoolmanIcon(checked)} />
</Form.Item>
{extraSettings}
</>
}
style={`
.print-page .print-qrcode-item {
display: flex;
width: 100%;
max-height: 100%;
justify-content: center;
}
.print-page .print-qrcode-container {
max-width: ${showContent ? "50%" : "100%"};
display: flex;
}
.print-page .print-qrcode {
width: auto !important;
height: auto !important;
}
.print-page .print-qrcode-title {
flex: 1 1 auto;
font-size: ${textSize}mm;
color: #000;
overflow: hidden;
}
.print-page canvas, .print-page svg {
/* display: block; */
object-fit: contain;
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
`}
onCancel={onCancel}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -1,142 +0,0 @@
import { Form, Switch } from "antd";
import { IFilament } from "../../pages/filaments/model";
import { ISpool } from "../../pages/spools/model";
import QRCodePrintingDialog from "./qrCodePrintingDialog";
import { useSavedState } from "../../utils/saveload";
import { useGetSetting } from "../../utils/querySettings";
import { useTranslate } from "@refinedev/core";
interface SpoolQRCodePrintingDialog {
visible: boolean;
items: ISpool[];
onCancel: () => void;
}
const SpoolQRCodePrintingDialog: React.FC<SpoolQRCodePrintingDialog> = ({ visible, items, onCancel }) => {
const t = useTranslate();
const baseUrlSetting = useGetSetting("qr_code_url");
const baseUrlRoot = baseUrlSetting.data?.value !== undefined && JSON.parse(baseUrlSetting.data?.value) !== "" ? JSON.parse(baseUrlSetting.data?.value) : window.location.origin;
const [showVendor, setShowVendor] = useSavedState("print-showVendor", true);
const [showLotNr, setShowLotNr] = useSavedState("print-showLotNr", true);
const [showSpoolWeight, setShowSpoolWeight] = useSavedState("print-showSpoolWeight", true);
const [showTemperatures, setShowTemperatures] = useSavedState("print-showTemperatures", true);
const [showSpoolComment, setShowSpoolComment] = useSavedState("print-showSpoolComment", false);
const [showFilamentComment, setShowFilamentComment] = useSavedState("print-showFilamentComment", false);
const [showVendorComment, setShowVendorComment] = useSavedState("print-showVendorComment", false);
const [useHTTPUrl, setUseHTTPUrl] = useSavedState("print-useHTTPUrl", baseUrlSetting.data?.value !== "");
const formatFilament = (filament: IFilament) => {
let vendorPrefix = "";
if (showVendor && filament.vendor) {
vendorPrefix = `${filament.vendor.name} - `;
}
let name = filament.name;
if (!name) {
name = `Filament #${filament.id}`;
}
return `${vendorPrefix}${name}`;
};
return (
<QRCodePrintingDialog
visible={visible}
onCancel={onCancel}
items={items.map(function (spool) {
const temps = [];
if (spool.filament.settings_extruder_temp) {
temps.push(t("printing.qrcode.extruderTemp", { temp: `${spool.filament.settings_extruder_temp} °C` }));
}
if (spool.filament.settings_bed_temp) {
temps.push(t("printing.qrcode.bedTemp", { temp: `${spool.filament.settings_bed_temp} °C` }));
}
const tempLine = temps.join(" - ");
return {
value: useHTTPUrl ? `${baseUrlRoot}/spool/show/${spool.id}` : `web+spoolman:s-${spool.id}`,
label: (
<p
style={{
padding: "1mm 1mm 1mm 0",
}}
>
<b>{formatFilament(spool.filament)}</b>
<br />
<b>
#{spool.id}
{spool.filament.material && <> - {spool.filament.material}</>}
</b>
{showSpoolWeight && (
<>
<br />
{t("printing.qrcode.spoolWeight", { weight: `${spool.filament.spool_weight ?? "?"} g` })}
</>
)}
{showTemperatures && tempLine && (
<>
<br />
{tempLine}
</>
)}
{showLotNr && (
<>
<br />
{t("printing.qrcode.lotNr", { lot: spool.lot_nr ?? "?" })}
</>
)}
{showSpoolComment && spool.comment && (
<>
<br />
{spool.comment}
</>
)}
{showFilamentComment && spool.filament.comment && (
<>
<br />
{spool.filament.comment}
</>
)}
{showVendorComment && spool.filament.vendor?.comment && (
<>
<br />
{spool.filament.vendor.comment}
</>
)}
</p>
),
errorLevel: "H",
};
})}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showVendor")}>
<Switch checked={showVendor} onChange={(checked) => setShowVendor(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolWeight")}>
<Switch checked={showSpoolWeight} onChange={(checked) => setShowSpoolWeight(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showTemperatures")}>
<Switch checked={showTemperatures} onChange={(checked) => setShowTemperatures(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showLotNr")}>
<Switch checked={showLotNr} onChange={(checked) => setShowLotNr(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolComment")}>
<Switch checked={showSpoolComment} onChange={(checked) => setShowSpoolComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showFilamentComment")}>
<Switch checked={showFilamentComment} onChange={(checked) => setShowFilamentComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.showVendorComment")}>
<Switch checked={showVendorComment} onChange={(checked) => setShowVendorComment(checked)} />
</Form.Item>
<Form.Item label={t("printing.qrcode.useHTTPUrl.label")} tooltip={t("printing.qrcode.useHTTPUrl.tooltip")}>
<Switch checked={useHTTPUrl} onChange={(checked) => setUseHTTPUrl(checked)} />
</Form.Item>
</>
}
/>
);
};
export default SpoolQRCodePrintingDialog;

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { FloatButton, Modal, Space } from "antd";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { useNavigate } from "react-router-dom";
import { CameraOutlined } from "@ant-design/icons";
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";
const QRCodeScannerModal: React.FC = () => {
const [visible, setVisible] = useState(false);

View File

@@ -1,50 +0,0 @@
// React FC that combines the functionality of first selecting what spools to print QR code for, and then opens up the QR code printing dialog.
import React from "react";
import SpoolSelectModal from "./spoolSelectModal";
import { Button } from "antd";
import { ISpool } from "../pages/spools/model";
import { PrinterOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
import SpoolQRCodePrintingDialog from "./printing/spoolQrCodePrintingDialog";
const SelectAndPrint: React.FC = () => {
const t = useTranslate();
const [step, setStep] = React.useState(0);
const [selectedSpools, setSelectedSpools] = React.useState<ISpool[]>([]);
return (
<>
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={() => {
setStep(1);
}}
>
{t("printing.qrcode.button")}
</Button>
<SpoolSelectModal
visible={step === 1}
description={t("printing.spoolSelect.description")}
onCancel={() => {
setStep(0);
}}
onContinue={(spools) => {
setSelectedSpools(spools);
setStep(2);
}}
/>
<SpoolQRCodePrintingDialog
visible={step === 2}
onCancel={() => {
setStep(0);
}}
items={selectedSpools}
/>
</>
);
};
export default SelectAndPrint;

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

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

View File

@@ -1,217 +0,0 @@
import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd";
import { ISpool } from "../pages/spools/model";
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "./column";
import { TableState } from "../utils/saveload";
import { useTable } from "@refinedev/antd";
import { t } from "i18next";
import { useSpoolmanFilamentFilter, useSpoolmanMaterials } from "./otherModels";
import { removeUndefined } from "../utils/filtering";
import { useNavigate } from "react-router-dom";
interface Props {
visible: boolean;
description?: string;
onCancel: () => void;
onContinue: (selectedSpools: ISpool[]) => void;
}
interface ISpoolCollapsed extends ISpool {
"filament.combined_name": string;
"filament.id": number;
"filament.material"?: string;
}
function collapseSpool(element: ISpool): ISpoolCollapsed {
let filament_name: string;
if (element.filament.vendor && "name" in element.filament.vendor) {
filament_name = `${element.filament.vendor.name} - ${element.filament.name}`;
} else {
filament_name = element.filament.name ?? element.filament.id.toString();
}
return {
...element,
"filament.combined_name": filament_name,
"filament.id": element.filament.id,
"filament.material": element.filament.material,
};
}
const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onContinue }) => {
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [showArchived, setShowArchived] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const navigate = useNavigate();
const { tableProps, sorters, filters, current, pageSize } = useTable<ISpoolCollapsed>({
meta: {
queryParams: {
["allow_archived"]: showArchived,
},
},
syncWithLocation: false,
pagination: {
mode: "off",
current: 1,
pageSize: 10,
},
sorters: {
mode: "server",
},
filters: {
mode: "server",
},
queryOptions: {
select(data) {
return {
total: data.total,
data: data.data.map(collapseSpool),
};
},
},
});
// Store state in local storage
const tableState: TableState = {
sorters,
filters,
pagination: { current, pageSize },
};
// Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: ISpoolCollapsed[] = React.useMemo(
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource]
);
// Function to add/remove all filtered items from selected items
const selectUnselectFiltered = (select: boolean) => {
setSelectedItems((prevSelected) => {
const filtered = dataSource.map((spool) => spool.id).filter((spool) => !prevSelected.includes(spool));
return select ? [...prevSelected, ...filtered] : filtered;
});
};
// Handler for selecting/unselecting individual items
const handleSelectItem = (item: number) => {
setSelectedItems((prevSelected) =>
prevSelected.includes(item) ? prevSelected.filter((selected) => selected !== item) : [...prevSelected, item]
);
};
// State for the select/unselect all checkbox
const isAllFilteredSelected = dataSource.every((spool) => selectedItems.includes(spool.id));
const isSomeButNotAllFilteredSelected =
dataSource.some((spool) => selectedItems.includes(spool.id)) && !isAllFilteredSelected;
const commonProps = {
t,
navigate,
actions: () => {
return [];
},
dataSource,
tableState,
sorter: true,
};
return (
<Modal
title={t("printing.spoolSelect.title")}
open={visible}
onCancel={onCancel}
onOk={() => {
if (selectedItems.length === 0) {
messageApi.open({
type: "error",
content: t("printing.spoolSelect.noSpoolsSelected"),
});
return;
}
onContinue(dataSource.filter((spool) => selectedItems.includes(spool.id)));
}}
width={600}
>
{contextHolder}
<Space direction="vertical" style={{ width: "100%" }}>
{description && <div>{description}</div>}
<Table
{...tableProps}
rowKey="id"
tableLayout="auto"
dataSource={dataSource}
pagination={false}
scroll={{ y: 200 }}
columns={removeUndefined([
{
width: 50,
render: (_, item: ISpool) => (
<Checkbox checked={selectedItems.includes(item.id)} onChange={() => handleSelectItem(item.id)} />
),
},
SortedColumn({
...commonProps,
id: "id",
i18ncat: "spool",
width: 80,
}),
SpoolIconColumn({
...commonProps,
id: "filament.combined_name",
dataId: "filament.combined_name",
i18nkey: "spool.fields.filament_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
filterValueQuery: useSpoolmanFilamentFilter(),
}),
FilteredQueryColumn({
...commonProps,
id: "filament.material",
i18nkey: "spool.fields.material",
filterValueQuery: useSpoolmanMaterials(),
}),
])}
/>
<Row>
<Col span={12}>
<Checkbox
checked={isAllFilteredSelected}
indeterminate={isSomeButNotAllFilteredSelected}
onChange={(e) => {
selectUnselectFiltered(e.target.checked);
}}
>
{t("printing.spoolSelect.selectAll")}
</Checkbox>
</Col>
<Col span={12}>
<div style={{ float: "right" }}>
{t("printing.spoolSelect.selectedTotal", {
count: selectedItems.length,
})}
</div>
</Col>
<Col span={12}>
<Checkbox
checked={showArchived}
onChange={(e) => {
setShowArchived(e.target.checked);
if (!e.target.checked) {
// Remove archived spools from selected items
setSelectedItems((prevSelected) =>
prevSelected.filter(
(selected) => dataSource.find((spool) => spool.id === selected)?.archived !== true
)
);
}
}}
>
{t("printing.spoolSelect.showArchived")}
</Checkbox>
</Col>
</Row>
</Space>
</Modal>
);
};
export default SpoolSelectModal;

View File

@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Spin, Typography } from "antd";
import { getAPIURL } from "../utils/url";
const { Text } = Typography;
@@ -15,12 +16,10 @@ interface IInfo {
}
export const Version: React.FC = () => {
const apiEndpoint = import.meta.env.VITE_APIURL;
const infoResult = useQuery<IInfo>({
queryKey: ["info"],
queryFn: async () => {
const response = await fetch(apiEndpoint + "/info");
const response = await fetch(getAPIURL() + "/info");
if (!response.ok) {
throw new Error("Network response was not ok");
}