Merge branch 'qrcode' into master

This commit is contained in:
Donkie
2023-08-08 21:32:41 +02:00
13 changed files with 1575 additions and 5 deletions

View File

@@ -15,6 +15,7 @@ import { ColorModeContext } from "../../contexts/color-mode";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
const { useToken } = theme;
@@ -84,6 +85,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
onChange={() => setMode(mode === "light" ? "dark" : "light")}
defaultChecked={mode === "dark"}
/>
<QRCodeScannerModal />
</Space>
</AntdLayout.Header>
);

View File

@@ -0,0 +1,640 @@
import React, { useRef } from "react";
import {
Modal,
Slider,
Button,
Select,
Row,
Col,
Form,
Divider,
RadioChangeEvent,
Radio,
InputNumber,
} 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 [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 [blockRowsTop, setBlockRowsTop] = useSavedState(
"print-blockRowsTop",
0
);
const [blockRowsBottom, setBlockRowsBottom] = useSavedState(
"print-blockRowsBottom",
0
);
const [blockColumnsLeft, setBlockColumnsLeft] = useSavedState(
"print-blockColumnsLeft",
0
);
const [blockColumnsRight, setBlockColumnsRight] = useSavedState(
"print-blockColumnsRight",
0
);
const [paperColumns, setPaperColumns] = useSavedState("print-itemsPerRow", 3);
const [paperRows, setPaperRows] = useSavedState("print-rowsPerPage", 8);
const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4");
const [previewScale, setPreviewScale] = useSavedState(
"print-previewScale",
0.6
);
const [borderShowMode, setBorderShowMode] = useSavedState<
"none" | "border" | "grid"
>("print-borderShowMode", "grid");
const paperWidth = paperDimensions[paperSize].width;
const paperHeight = paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const calculatedRowHeight =
(paperHeight - marginTop - marginBottom) / paperRows;
const itemsPerRow = paperColumns - blockColumnsLeft - blockColumnsRight;
const rowsPerPage = paperRows - blockRowsTop - blockRowsBottom;
const rowsOfItems = [];
for (let row_idx = 0; row_idx <= items.length / itemsPerRow; row_idx += 1) {
rowsOfItems.push(
items.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, idx) {
const itemRowsBlockTop = [...Array(blockRowsTop).keys()].map((row) => (
<tr key={row}>
<td>
<div style={{ height: `${calculatedRowHeight}mm` }}></div>
</td>
</tr>
));
const itemRows = rows.map((row, idx) => {
const itemColumnsBlockLeft = [...Array(blockColumnsLeft).keys()].map(
(col) => (
<td key={col}>
<div
style={{
width: `${
(paperWidth - marginLeft - marginRight) / paperColumns
}mm`,
}}
></div>
</td>
)
);
return (
<tr key={idx}>
{itemColumnsBlockLeft}
{row.map((item, index) => (
<td>
<div
key={index}
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: `${
(paperWidth - marginLeft - marginRight) / paperColumns
}mm`,
height: `${calculatedRowHeight}mm`,
flexDirection: "column",
border: borderShowMode === "grid" ? "1px solid #000" : "none",
}}
>
{item}
</div>
</td>
))}
</tr>
);
});
return (
<div
className="print-page"
key={idx}
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",
}}
>
{itemRowsBlockTop}
{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}>{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 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.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>
<Form.Item label={t("printing.generic.columns")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={5}
value={paperColumns}
onChange={(value) => {
if (value - blockColumnsLeft - blockColumnsRight < 1)
return;
setPaperColumns(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperColumns}
onChange={(value) => {
if (
(value ?? 1) - blockColumnsLeft - blockColumnsRight <
1
)
return;
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) => {
if (value - blockRowsTop - blockRowsBottom < 1) return;
setPaperRows(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperRows}
onChange={(value) => {
if ((value ?? 1) - blockRowsTop - blockRowsBottom < 1)
return;
setPaperRows(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<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>
</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.blockRowsTop")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockRowsTop}
onChange={(value) => {
if (paperRows - value - blockRowsBottom < 1) return;
setBlockRowsTop(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockRowsTop}
onChange={(value) => {
if (paperRows - (value ?? 0) - blockRowsBottom < 1)
return;
setBlockRowsTop(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockRowsBottom")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockRowsBottom}
onChange={(value) => {
if (paperRows - value - blockRowsTop < 1) return;
setBlockRowsBottom(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockRowsBottom}
onChange={(value) => {
if (paperRows - (value ?? 0) - blockRowsTop < 1) return;
setBlockRowsBottom(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockColumnsLeft")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockColumnsLeft}
onChange={(value) => {
if (paperColumns - value - blockColumnsRight < 1) return;
setBlockColumnsLeft(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockColumnsLeft}
onChange={(value) => {
if (paperColumns - (value ?? 0) - blockColumnsRight < 1)
return;
setBlockColumnsLeft(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockColumnsRight")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockColumnsRight}
onChange={(value) => {
if (paperColumns - value - blockColumnsLeft < 1) return;
setBlockColumnsRight(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockColumnsRight}
onChange={(value) => {
if (paperColumns - (value ?? 0) - blockColumnsLeft < 1)
return;
setBlockColumnsRight(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
{extraSettings && <Divider />}
{extraSettings}
<Divider />
<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>
</Form>
</Col>
</Row>
</Modal>
);
};
export default PrintingDialog;

View File

@@ -0,0 +1,130 @@
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?: string;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
visible: boolean;
items: QRCodeData[];
onCancel: () => void;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
visible,
items,
onCancel,
}) => {
const t = useTranslate();
const [showContent, setShowContent] = useSavedState(
"print-showContent",
true
);
const [textSize, setTextSize] = useSavedState("print-textSize", 5);
const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState(
"print-showSpoolmanIcon",
true
);
const elements = items.map((item) => {
return (
<>
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.ico" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
{showContent && (
<div
className="print-qrcode-title"
style={{ textAlign: "center", color: "#000" }}
>
{item.label ?? item.value}
</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={3}
max={15}
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>
</>
}
style={`
.print-page .print-qrcode {
height: 100% !important;
width: 100% !important;
}
.print-page .print-qrcode-title {
font-size: ${textSize}mm;
}
.print-page svg {
display: block;
height: 100%;
width: auto;
}
`}
onCancel={onCancel}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -0,0 +1,82 @@
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";
const QRCodeScannerModal: React.FC = () => {
const [visible, setVisible] = useState(false);
const [lastError, setLastError] = useState<string | null>(null);
const t = useTranslate();
const navigate = useNavigate();
const onScan = (result: string) => {
// Check for the spoolman ID format
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/);
if (match && match.groups) {
setVisible(false);
navigate(`/spool/show/${match.groups.id}`);
}
};
return (
<>
<FloatButton
type="primary"
onClick={() => setVisible(true)}
icon={<CameraOutlined />}
shape="circle"
/>
<Modal
open={visible}
onCancel={() => setVisible(false)}
footer={null}
title={t("scanner.title")}
>
<Space direction="vertical" style={{ width: "100%" }}>
<p>{t("scanner.description")}</p>
<QrScanner
viewFinder={
lastError
? () => (
<div
style={{
position: "absolute",
textAlign: "center",
width: "100%",
top: "50%",
}}
>
<p>{lastError}</p>
</div>
)
: undefined
}
onDecode={onScan}
onError={(error: Error) => {
console.error(error);
if (error.name === "NotAllowedError") {
setLastError(t("scanner.error.notAllowed"));
} else if (
error.name === "InsecureContextError" ||
(location.protocol !== "https:" &&
navigator.mediaDevices === undefined)
) {
setLastError(t("scanner.error.insecureContext"));
} else if (error.name === "StreamApiNotSupportedError") {
setLastError(t("scanner.error.streamApiNotSupported"));
} else if (error.name === "NotReadableError") {
setLastError(t("scanner.error.notReadable"));
} else {
setLastError(t("scanner.error.unknown", { error: error.name }));
}
}}
/>
</Space>
</Modal>
</>
);
};
export default QRCodeScannerModal;

View File

@@ -0,0 +1,53 @@
// 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 QRCodePrintingDialog from "./printing/qrCodePrintingDialog";
import { Button } from "antd";
import { ISpool } from "../pages/spools/model";
import { PrinterOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
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);
}}
/>
<QRCodePrintingDialog
visible={step === 2}
onCancel={() => {
setStep(1);
}}
items={selectedSpools.map((spool) => ({
value: `web+spoolman:s-${spool.id}`,
label: `s-${spool.id}`,
errorLevel: "H",
}))}
/>
</>
);
};
export default SelectAndPrint;

View File

@@ -0,0 +1,217 @@
import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd";
import { ISpool } from "../pages/spools/model";
import { FilteredColumn, SortedColumn, SpoolIconColumn } from "./column";
import { TableState } from "../utils/saveload";
import { useTable } from "@refinedev/antd";
import { genericSorter, typeSorters } from "../utils/sorting";
import { genericFilterer, typeFilters } from "../utils/filtering";
import { t } from "i18next";
interface Props {
visible: boolean;
description?: string;
onCancel: () => void;
onContinue: (selectedSpools: ISpool[]) => void;
}
interface ISpoolCollapsed extends ISpool {
filament_name: string;
material?: string;
}
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 { tableProps, sorters, filters, current, pageSize } = useTable<ISpool>({
meta: {
queryParams: {
["allow_archived"]: showArchived,
},
},
syncWithLocation: false,
pagination: {
mode: "off",
current: 1,
pageSize: 10,
},
sorters: {
mode: "off",
},
filters: {
mode: "off",
},
});
// 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((element) => {
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_name,
material: element.filament.material,
};
}),
[tableProps.dataSource]
);
// Type the sorters and filters
const typedSorters = typeSorters<ISpoolCollapsed>(sorters);
const typedFilters = typeFilters<ISpoolCollapsed>(filters);
// Filter and sort the dataSource
const filteredDataSource = React.useMemo(() => {
const filtered = dataSource.filter(genericFilterer(typedFilters));
filtered.sort(genericSorter(typedSorters));
return filtered;
}, [dataSource, typedFilters, typedSorters]);
// Function to add/remove all filtered items from selected items
const selectUnselectFiltered = (select: boolean) => {
setSelectedItems((prevSelected) => {
const filtered = filteredDataSource
.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 = filteredDataSource.every((spool) =>
selectedItems.includes(spool.id)
);
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"
dataSource={filteredDataSource}
pagination={false}
scroll={{ x: "max-content", y: 200 }}
>
<Table.Column
width={50}
render={(_, item: ISpool) => (
<Checkbox
checked={selectedItems.includes(item.id)}
onChange={() => handleSelectItem(item.id)}
/>
)}
/>
{SortedColumn({
id: "id",
i18ncat: "spool",
dataSource,
tableState,
})}
{SpoolIconColumn({
id: "filament_name",
i18ncat: "spool",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
dataSource,
tableState,
})}
{FilteredColumn({
id: "material",
i18ncat: "spool",
dataSource,
tableState,
})}
</Table>
<Row>
<Col span={12}>
<Checkbox
checked={isAllFilteredSelected}
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

@@ -37,6 +37,7 @@ import {
SpoolIconColumn,
} from "../../components/column";
import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog";
dayjs.extend(utc);
@@ -191,6 +192,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
<List
headerButtons={({ defaultButtons }) => (
<>
<SelectAndPrint />
<Button
type="primary"
icon={<InboxOutlined />}