Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
20
client/src/components/favicon.tsx
Normal file
20
client/src/components/favicon.tsx
Normal 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 <></>;
|
||||
}
|
||||
72
client/src/components/filamentImportModal.tsx
Normal file
72
client/src/components/filamentImportModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
83
client/src/components/multiColorPicker.tsx
Normal file
83
client/src/components/multiColorPicker.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
41
client/src/components/spoolIcon.css
Normal file
41
client/src/components/spoolIcon.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.spool-icon {
|
||||
display: flex;
|
||||
width: 1.5em;
|
||||
height: 2.5em;
|
||||
gap: 2px;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.spool-icon.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spool-icon.large {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.spool-icon * {
|
||||
flex: 1 1 0px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.spool-icon.vertical *:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.spool-icon.vertical *:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.spool-icon.horizontal *:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
.spool-icon.horizontal *:last-child {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
32
client/src/components/spoolIcon.tsx
Normal file
32
client/src/components/spoolIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import "./spoolIcon.css";
|
||||
|
||||
interface Props {
|
||||
color: string | { colors: string[]; vertical: boolean };
|
||||
size?: "small" | "large";
|
||||
}
|
||||
|
||||
export default function SpoolIcon(props: Props) {
|
||||
let dirClass = "vertical";
|
||||
let cols = [];
|
||||
let size = props.size ? props.size : "small";
|
||||
|
||||
if (typeof props.color === "string") {
|
||||
cols = [props.color];
|
||||
} else {
|
||||
dirClass = props.color.vertical ? "vertical" : "horizontal";
|
||||
cols = props.color.colors;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"spool-icon " + dirClass + " " + size}>
|
||||
{cols.map((col) => (
|
||||
<div
|
||||
key={col}
|
||||
style={{
|
||||
backgroundColor: "#" + col.replace("#", ""),
|
||||
}}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,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;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user