@@ -1,7 +1,7 @@
|
||||
import { Col, Row, Table } from "antd";
|
||||
import { ColumnProps as AntdColumnProps } from "antd/es/table";
|
||||
import { ColumnFilterItem } from "antd/es/table/interface";
|
||||
import { getFiltersForField, typeFilters, useListFiltersForField } from "../utils/filtering";
|
||||
import { getFiltersForField, typeFilters } from "../utils/filtering";
|
||||
import { TableState } from "../utils/saveload";
|
||||
import { getSortOrderForField, typeSorters } from "../utils/sorting";
|
||||
import { NumberFieldUnit } from "./numberField";
|
||||
@@ -12,12 +12,14 @@ import Icon from "@ant-design/icons";
|
||||
import { ReactComponent as SpoolIcon } from "../icon_spool.svg";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { enrichText } from "../utils/parsing";
|
||||
import { UseQueryResult } from "@tanstack/react-query";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
interface BaseColumnProps<Obj> {
|
||||
id: keyof Obj & string;
|
||||
i18ncat: string;
|
||||
i18ncat?: string;
|
||||
i18nkey?: string;
|
||||
dataSource: Obj[];
|
||||
tableState: TableState;
|
||||
}
|
||||
@@ -25,6 +27,7 @@ interface BaseColumnProps<Obj> {
|
||||
interface FilteredColumnProps {
|
||||
filters?: ColumnFilterItem[];
|
||||
filteredValue?: string[];
|
||||
allowMultipleFilters?: boolean;
|
||||
}
|
||||
|
||||
interface CustomColumnProps<Obj> {
|
||||
@@ -45,9 +48,10 @@ function Column<Obj>(props: BaseColumnProps<Obj> & FilteredColumnProps & CustomC
|
||||
const typedSorters = typeSorters<Obj>(props.tableState.sorters);
|
||||
const columnProps: AntdColumnProps<Obj> = {
|
||||
dataIndex: props.id,
|
||||
title: t(`${props.i18ncat}.fields.${props.id}`),
|
||||
title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
|
||||
sorter: true,
|
||||
sortOrder: getSortOrderForField(typedSorters, props.id),
|
||||
filterMultiple: props.allowMultipleFilters ?? true,
|
||||
};
|
||||
if (props.filters && props.filteredValue) {
|
||||
columnProps.filters = props.filters;
|
||||
@@ -75,10 +79,29 @@ export function RichColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
});
|
||||
}
|
||||
|
||||
export function FilteredColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
interface FilteredQueryColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||
filterValueQuery: UseQueryResult<string[]>;
|
||||
allowMultipleFilters?: boolean;
|
||||
}
|
||||
|
||||
const filters = useListFiltersForField(props.dataSource, props.id);
|
||||
export function FilteredQueryColumn<Obj>(props: FilteredQueryColumnProps<Obj>) {
|
||||
const query = props.filterValueQuery;
|
||||
|
||||
let filters: ColumnFilterItem[] = [];
|
||||
if (query.data) {
|
||||
filters = query.data.map((item) => {
|
||||
return {
|
||||
text: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
filters.push({
|
||||
text: "<empty>",
|
||||
value: "",
|
||||
});
|
||||
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.id);
|
||||
|
||||
return Column({ ...props, filters, filteredValue });
|
||||
@@ -127,14 +150,28 @@ export function DateColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
});
|
||||
}
|
||||
|
||||
interface SpoolIconColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||
interface SpoolIconColumnProps<Obj> extends FilteredQueryColumnProps<Obj> {
|
||||
color: (record: Obj) => string | undefined;
|
||||
}
|
||||
|
||||
export function SpoolIconColumn<Obj>(props: SpoolIconColumnProps<Obj>) {
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const query = props.filterValueQuery;
|
||||
|
||||
const filters = useListFiltersForField(props.dataSource, props.id);
|
||||
let filters: ColumnFilterItem[] = [];
|
||||
if (query.data) {
|
||||
filters = query.data.map((item) => {
|
||||
return {
|
||||
text: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
filters.push({
|
||||
text: "<empty>",
|
||||
value: "",
|
||||
});
|
||||
|
||||
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||
const filteredValue = getFiltersForField(typedFilters, props.id);
|
||||
|
||||
return Column({
|
||||
|
||||
@@ -16,7 +16,8 @@ const dataProvider = (
|
||||
getList: async ({ resource, meta, pagination, sorters, filters }) => {
|
||||
const url = `${apiUrl}/${resource}`;
|
||||
|
||||
const { headers: headersFromMeta, method, queryParams } = meta ?? {};
|
||||
const { headers: headersFromMeta, method } = meta ?? {};
|
||||
const queryParams: Record<string, string | number> = meta?.queryParams ?? {};
|
||||
const requestMethod = (method as MethodTypes) ?? "get";
|
||||
|
||||
if (pagination && pagination.mode == "server") {
|
||||
@@ -28,22 +29,60 @@ const dataProvider = (
|
||||
|
||||
if (sorters && sorters.length > 0) {
|
||||
queryParams["sort"] = sorters.map((sort) => {
|
||||
const field = sort.field.replace("_", ".")
|
||||
const field = sort.field
|
||||
return `${field}:${sort.order}`;
|
||||
}).join(",")
|
||||
}
|
||||
|
||||
if (filters && filters.length > 0) {
|
||||
// Pre-process the filters and handle the special case where we want to split the filament name into vendor and filament name
|
||||
if (resource === "spool") {
|
||||
filters = structuredClone(filters)
|
||||
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
const filter = filters[i]
|
||||
if (("field" in filter) && filter.field === "filament.name") {
|
||||
const filamentNames: string[] = []
|
||||
const vendorNames: string[] = []
|
||||
if (Array.isArray(filter.value)) {
|
||||
filter.value.forEach((value: string) => {
|
||||
const split = value.split(" - ")
|
||||
if (split.length === 1) {
|
||||
filamentNames.push(split[0])
|
||||
} else {
|
||||
vendorNames.push(split[0])
|
||||
filamentNames.push(split[1])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
filamentNames.push(filter.value.split(" - ")[1])
|
||||
vendorNames.push(filter.value.split(" - ")[0])
|
||||
}
|
||||
filter.value = filamentNames
|
||||
|
||||
filters.push({
|
||||
field: "vendor.name",
|
||||
operator: "in",
|
||||
value: vendorNames,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filters.forEach(filter => {
|
||||
if (!("field" in filter)) {
|
||||
throw Error("Filter must be a LogicalFilter.")
|
||||
}
|
||||
const field = filter.field.replace(".", "_")
|
||||
queryParams[field] = filter.value
|
||||
const field = filter.field
|
||||
const fieldValue = Array.isArray(filter.value) ? filter.value.join(",") : filter.value
|
||||
if (fieldValue.length > 0) {
|
||||
queryParams[field] = fieldValue
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await httpClient[requestMethod](
|
||||
const { data, headers } = await httpClient[requestMethod](
|
||||
`${url}`,
|
||||
{
|
||||
headers: headersFromMeta,
|
||||
@@ -51,10 +90,11 @@ const dataProvider = (
|
||||
},
|
||||
);
|
||||
|
||||
// console.log(url, requestMethod, queryParams, data, headers)
|
||||
|
||||
return {
|
||||
data,
|
||||
total: 100,
|
||||
//TODO: total: data.length,
|
||||
total: parseInt(headers["x-total-count"]) ?? 100,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
152
client/src/components/otherModels.tsx
Normal file
152
client/src/components/otherModels.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { IFilament } from "../pages/filaments/model";
|
||||
import { IVendor } from "../pages/vendors/model";
|
||||
|
||||
export function useSpoolmanFilamentFullNames() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<IFilament[], unknown, string[]>({
|
||||
queryKey: ["filaments"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/filament");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
// Concatenate vendor name and filament name
|
||||
let names = data
|
||||
.filter((filament) => {
|
||||
return filament.name !== null && filament.name !== undefined && filament.name !== "";
|
||||
})
|
||||
.map((filament) => {
|
||||
if (filament.vendor?.name) {
|
||||
return `${filament.vendor.name} - ${filament.name ?? "<unknown>"}`;
|
||||
} else {
|
||||
return `${filament.name ?? "<unknown>"}`;
|
||||
}
|
||||
})
|
||||
.sort();
|
||||
// Remove duplicates
|
||||
names = [...new Set(names)];
|
||||
return names;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanFilamentNames() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<IFilament[], unknown, string[]>({
|
||||
queryKey: ["filaments"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/filament");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
// Concatenate vendor name and filament name
|
||||
let names = data
|
||||
.filter((filament) => {
|
||||
return filament.name !== null && filament.name !== undefined && filament.name !== "";
|
||||
})
|
||||
.map((filament) => {
|
||||
return filament.name ?? "<unknown>";
|
||||
})
|
||||
.sort();
|
||||
// Remove duplicates
|
||||
names = [...new Set(names)];
|
||||
return names;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanVendors() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<IVendor[], unknown, string[]>({
|
||||
queryKey: ["vendors"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/vendor");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data
|
||||
.map((vendor) => {
|
||||
return vendor.name ?? `ID ${vendor.id}`;
|
||||
})
|
||||
.sort();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanMaterials() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<string[]>({
|
||||
queryKey: ["materials"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/material");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data.sort();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanArticleNumbers() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<string[]>({
|
||||
queryKey: ["articleNumbers"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/article-number");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data.sort();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanLotNumbers() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<string[]>({
|
||||
queryKey: ["lotNumbers"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/lot-number");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data.sort();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSpoolmanLocations() {
|
||||
const apiEndpoint = import.meta.env.VITE_APIURL;
|
||||
return useQuery<string[]>({
|
||||
queryKey: ["locations"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(apiEndpoint + "/location");
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
select: (data) => {
|
||||
return data.sort();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
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 { FilteredQueryColumn, 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";
|
||||
import { useSpoolmanFilamentFullNames, useSpoolmanMaterials } from "./otherModels";
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -16,8 +15,8 @@ interface Props {
|
||||
}
|
||||
|
||||
interface ISpoolCollapsed extends ISpool {
|
||||
filament_name: string;
|
||||
material?: string;
|
||||
"filament.name": string;
|
||||
"filament.material"?: string;
|
||||
}
|
||||
|
||||
const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onContinue }) => {
|
||||
@@ -64,28 +63,17 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
|
||||
}
|
||||
return {
|
||||
...element,
|
||||
filament_name,
|
||||
material: element.filament.material,
|
||||
"filament.name": filament_name,
|
||||
"filament.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));
|
||||
const filtered = dataSource.map((spool) => spool.id).filter((spool) => !prevSelected.includes(spool));
|
||||
return select ? [...prevSelected, ...filtered] : filtered;
|
||||
});
|
||||
};
|
||||
@@ -98,7 +86,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
|
||||
};
|
||||
|
||||
// State for the select/unselect all checkbox
|
||||
const isAllFilteredSelected = filteredDataSource.every((spool) => selectedItems.includes(spool.id));
|
||||
const isAllFilteredSelected = dataSource.every((spool) => selectedItems.includes(spool.id));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -123,7 +111,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
|
||||
<Table
|
||||
{...tableProps}
|
||||
rowKey="id"
|
||||
dataSource={filteredDataSource}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
scroll={{ x: "max-content", y: 200 }}
|
||||
>
|
||||
@@ -140,17 +128,19 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
|
||||
tableState,
|
||||
})}
|
||||
{SpoolIconColumn({
|
||||
id: "filament_name",
|
||||
id: "filament.name",
|
||||
i18ncat: "spool",
|
||||
color: (record: ISpoolCollapsed) => record.filament.color_hex,
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanFilamentFullNames(),
|
||||
})}
|
||||
{FilteredColumn({
|
||||
id: "material",
|
||||
i18ncat: "spool",
|
||||
{FilteredQueryColumn({
|
||||
id: "filament.material",
|
||||
i18nkey: "spool.fields.material",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanMaterials(),
|
||||
})}
|
||||
</Table>
|
||||
<Row>
|
||||
|
||||
Reference in New Issue
Block a user