@@ -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>
|
||||
|
||||
@@ -5,54 +5,59 @@ import { Table, Space, Button, Dropdown } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { IFilament } from "./model";
|
||||
import { genericSorter, typeSorters } from "../../utils/sorting";
|
||||
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||
import { EditOutlined, FilterOutlined } from "@ant-design/icons";
|
||||
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
|
||||
import {
|
||||
DateColumn,
|
||||
FilteredColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
SpoolIconColumn,
|
||||
} from "../../components/column";
|
||||
import {
|
||||
useSpoolmanArticleNumbers,
|
||||
useSpoolmanFilamentNames,
|
||||
useSpoolmanMaterials,
|
||||
useSpoolmanVendors,
|
||||
} from "../../components/otherModels";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
interface IFilamentCollapsed extends Omit<IFilament, "vendor"> {
|
||||
vendor_name: string | null;
|
||||
"vendor.name": string | null;
|
||||
}
|
||||
|
||||
const namespace = "filamentList-v2";
|
||||
|
||||
export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
const t = useTranslate();
|
||||
|
||||
// Load initial state
|
||||
const initialState = useInitialTableState("filamentList");
|
||||
const initialState = useInitialTableState(namespace);
|
||||
|
||||
// Fetch data from the API
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } =
|
||||
useTable<IFilament>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "off", // Perform pagination in antd's Table instead. Otherwise client-side sorting/filtering doesn't work.
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "off", // Disable server-side sorting
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "off", // Disable server-side filtering
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IFilament>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
|
||||
// Create state for the columns to show
|
||||
const allColumns: (keyof IFilamentCollapsed & string)[] = [
|
||||
"id",
|
||||
"vendor_name",
|
||||
"vendor.name",
|
||||
"name",
|
||||
"material",
|
||||
"price",
|
||||
@@ -72,10 +77,6 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
|
||||
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns);
|
||||
|
||||
// Type the sorters and filters
|
||||
const typedSorters = typeSorters<IFilamentCollapsed>(sorters);
|
||||
const typedFilters = typeFilters<IFilamentCollapsed>(filters);
|
||||
|
||||
// Store state in local storage
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
@@ -83,7 +84,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
pagination: { current, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState("filamentList", tableState);
|
||||
useStoreInitialState(namespace, tableState);
|
||||
|
||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||
const dataSource: IFilamentCollapsed[] = React.useMemo(
|
||||
@@ -95,17 +96,14 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
} else {
|
||||
vendor_name = null;
|
||||
}
|
||||
return { ...element, vendor_name };
|
||||
return { ...element, "vendor.name": vendor_name };
|
||||
}),
|
||||
[tableProps.dataSource]
|
||||
);
|
||||
|
||||
// Filter and sort the dataSource
|
||||
const filteredDataSource = React.useMemo(() => {
|
||||
const filtered = dataSource.filter(genericFilterer(typedFilters));
|
||||
filtered.sort(genericSorter(typedSorters));
|
||||
return filtered;
|
||||
}, [dataSource, typedFilters, typedSorters]);
|
||||
if (tableProps.pagination) {
|
||||
tableProps.pagination.showSizeChanger = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -148,31 +146,20 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
{...tableProps}
|
||||
dataSource={filteredDataSource}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
current: current,
|
||||
pageSize: pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setCurrent(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
>
|
||||
<Table {...tableProps} dataSource={dataSource} rowKey="id">
|
||||
{SortedColumn({
|
||||
id: "id",
|
||||
i18ncat: "filament",
|
||||
dataSource,
|
||||
tableState,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
id: "vendor_name",
|
||||
i18ncat: "filament",
|
||||
{FilteredQueryColumn({
|
||||
id: "vendor.name",
|
||||
i18nkey: "filament.fields.vendor_name",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanVendors(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{SpoolIconColumn({
|
||||
id: "name",
|
||||
@@ -180,12 +167,16 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
color: (record: IFilamentCollapsed) => record.color_hex,
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanFilamentNames(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
{FilteredQueryColumn({
|
||||
id: "material",
|
||||
i18ncat: "filament",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanMaterials(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{SortedColumn({
|
||||
id: "price",
|
||||
@@ -225,11 +216,13 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
dataSource,
|
||||
tableState,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
{FilteredQueryColumn({
|
||||
id: "article_number",
|
||||
i18ncat: "filament",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanArticleNumbers(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{NumberColumn({
|
||||
id: "settings_extruder_temp",
|
||||
|
||||
@@ -4,14 +4,12 @@ import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/
|
||||
import { Table, Space, Button, Dropdown, Modal } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { genericSorter, typeSorters } from "../../utils/sorting";
|
||||
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||
import { ISpool } from "./model";
|
||||
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
|
||||
import { EditOutlined, FilterOutlined, InboxOutlined, ToTopOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
DateColumn,
|
||||
FilteredColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
@@ -19,55 +17,62 @@ import {
|
||||
} from "../../components/column";
|
||||
import { setSpoolArchived } from "./functions";
|
||||
import SelectAndPrint from "../../components/selectAndPrintDialog";
|
||||
import {
|
||||
useSpoolmanFilamentFullNames,
|
||||
useSpoolmanLocations,
|
||||
useSpoolmanLotNumbers,
|
||||
useSpoolmanMaterials,
|
||||
} from "../../components/otherModels";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface ISpoolCollapsed extends ISpool {
|
||||
filament_name: string;
|
||||
filament_material?: string;
|
||||
"filament.name": string;
|
||||
"filament.material"?: string;
|
||||
}
|
||||
|
||||
const namespace = "spoolList-v2";
|
||||
|
||||
export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
const t = useTranslate();
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
// Load initial state
|
||||
const initialState = useInitialTableState("spoolList");
|
||||
const initialState = useInitialTableState(namespace);
|
||||
|
||||
// State for the switch to show archived spools
|
||||
const [showArchived, setShowArchived] = useSavedState("spoolList-showArchived", false);
|
||||
|
||||
// Fetch data from the API
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } =
|
||||
useTable<ISpool>({
|
||||
meta: {
|
||||
queryParams: {
|
||||
["allow_archived"]: showArchived,
|
||||
},
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<ISpool>({
|
||||
meta: {
|
||||
queryParams: {
|
||||
["allow_archived"]: showArchived,
|
||||
},
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
},
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
|
||||
// Create state for the columns to show
|
||||
const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
||||
"id",
|
||||
"filament_name",
|
||||
"filament_material",
|
||||
"filament.name",
|
||||
"filament.material",
|
||||
"used_weight",
|
||||
"remaining_weight",
|
||||
"used_length",
|
||||
@@ -91,7 +96,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
pagination: { current, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState("spoolList", tableState);
|
||||
useStoreInitialState(namespace, tableState);
|
||||
|
||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||
const dataSource: ISpoolCollapsed[] = React.useMemo(
|
||||
@@ -105,8 +110,8 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
}
|
||||
return {
|
||||
...element,
|
||||
filament_name,
|
||||
filament_material: element.filament.material,
|
||||
"filament.name": filament_name,
|
||||
"filament.material": element.filament.material,
|
||||
};
|
||||
}),
|
||||
[tableProps.dataSource]
|
||||
@@ -143,6 +148,10 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (tableProps.pagination) {
|
||||
tableProps.pagination.showSizeChanger = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
headerButtons={({ defaultButtons }) => (
|
||||
@@ -219,17 +228,21 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
tableState,
|
||||
})}
|
||||
{SpoolIconColumn({
|
||||
id: "filament_name",
|
||||
i18ncat: "spool",
|
||||
id: "filament.name",
|
||||
i18nkey: "spool.fields.filament_name",
|
||||
color: (record: ISpoolCollapsed) => record.filament.color_hex,
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanFilamentFullNames(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
id: "filament_material",
|
||||
i18ncat: "spool",
|
||||
{FilteredQueryColumn({
|
||||
id: "filament.material",
|
||||
i18nkey: "spool.fields.material",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanMaterials(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{NumberColumn({
|
||||
id: "used_weight",
|
||||
@@ -265,17 +278,21 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
dataSource,
|
||||
tableState,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
{FilteredQueryColumn({
|
||||
id: "location",
|
||||
i18ncat: "spool",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanLocations(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{FilteredColumn({
|
||||
{FilteredQueryColumn({
|
||||
id: "lot_nr",
|
||||
i18ncat: "spool",
|
||||
dataSource,
|
||||
tableState,
|
||||
filterValueQuery: useSpoolmanLotNumbers(),
|
||||
allowMultipleFilters: false,
|
||||
})}
|
||||
{DateColumn({
|
||||
id: "first_used",
|
||||
|
||||
69
client/src/pages/vendors/list.tsx
vendored
69
client/src/pages/vendors/list.tsx
vendored
@@ -4,8 +4,6 @@ import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/
|
||||
import { Table, Space, Button, Dropdown } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { genericSorter, typeSorters } from "../../utils/sorting";
|
||||
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||
import { IVendor } from "./model";
|
||||
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
|
||||
import { EditOutlined, FilterOutlined } from "@ant-design/icons";
|
||||
@@ -13,39 +11,36 @@ import { DateColumn, RichColumn, SortedColumn } from "../../components/column";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const namespace = "vendorList-v2";
|
||||
|
||||
export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
const t = useTranslate();
|
||||
|
||||
// Load initial state
|
||||
const initialState = useInitialTableState("vendorList");
|
||||
const initialState = useInitialTableState(namespace);
|
||||
|
||||
// Fetch data from the API
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } =
|
||||
useTable<IVendor>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "off", // Perform pagination in antd's Table instead. Otherwise client-side sorting/filtering doesn't work.
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "off", // Disable server-side sorting
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "off", // Disable server-side filtering
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IVendor>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
});
|
||||
|
||||
// Create state for the columns to show
|
||||
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"];
|
||||
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? allColumns);
|
||||
|
||||
// Type the sorters and filters
|
||||
const typedSorters = typeSorters<IVendor>(sorters);
|
||||
const typedFilters = typeFilters<IVendor>(filters);
|
||||
|
||||
// Store state in local storage
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
@@ -53,7 +48,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
pagination: { current, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState("vendorList", tableState);
|
||||
useStoreInitialState(namespace, tableState);
|
||||
|
||||
// Collapse the dataSource to a mutable list
|
||||
const dataSource: IVendor[] = React.useMemo(
|
||||
@@ -61,12 +56,9 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
[tableProps.dataSource]
|
||||
);
|
||||
|
||||
// Filter and sort the dataSource
|
||||
const filteredDataSource = React.useMemo(() => {
|
||||
const filtered = dataSource.filter(genericFilterer(typedFilters));
|
||||
filtered.sort(genericSorter(typedSorters));
|
||||
return filtered;
|
||||
}, [dataSource, typedFilters, typedSorters]);
|
||||
if (tableProps.pagination) {
|
||||
tableProps.pagination.showSizeChanger = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -109,20 +101,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
{...tableProps}
|
||||
dataSource={filteredDataSource}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
current: current,
|
||||
pageSize: pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setCurrent(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
>
|
||||
<Table {...tableProps} dataSource={dataSource} rowKey="id">
|
||||
{SortedColumn({
|
||||
id: "id",
|
||||
i18ncat: "vendor",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { CrudFilter, CrudOperators } from "@refinedev/core";
|
||||
import { ColumnFilterItem } from "antd/es/table/interface";
|
||||
import React from "react";
|
||||
|
||||
interface TypedCrudFilter<Obj> {
|
||||
field: keyof Obj;
|
||||
@@ -12,92 +10,6 @@ export function typeFilters<Obj>(filters: CrudFilter[]): TypedCrudFilter<Obj>[]
|
||||
return filters as TypedCrudFilter<Obj>[]; // <-- Unsafe cast
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtering function that can be used to filter an array of objects based on the provided filters.
|
||||
* @param filters An array of `CrudFilter` objects that define the filtering criteria.
|
||||
* @returns A function that returns a boolean value based on whether the provided object matches the filtering criteria.
|
||||
*/
|
||||
export function genericFilterer<Obj>(filters: TypedCrudFilter<Obj>[]) {
|
||||
return (record: Obj) => {
|
||||
for (const filter of filters) {
|
||||
if (!("field" in filter)) {
|
||||
console.error("Filter must be of type LogicalFilter");
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(filter.value)) {
|
||||
console.error("Filter value must be an array of strings.");
|
||||
return false;
|
||||
}
|
||||
if (!filter.value.length) {
|
||||
continue;
|
||||
}
|
||||
const value = record[filter.field];
|
||||
let strValue: string;
|
||||
if (value === undefined || value === null) {
|
||||
strValue = "";
|
||||
} else {
|
||||
if (typeof value !== "string") {
|
||||
console.error("Only string fields can be filtered, field is of type " + typeof value);
|
||||
return false;
|
||||
} else {
|
||||
strValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case "in":
|
||||
if (!filter.value.includes(strValue)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`Not implemented operator: ${filter.operator}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates an array of `ColumnFilterItem` objects based on the unique values of a given field in an array of objects.
|
||||
* @param dataSource An array of objects to filter.
|
||||
* @param field The name of the field to filter on.
|
||||
* @returns An array of `ColumnFilterItem` objects representing the unique values of the specified field in the input array.
|
||||
*/
|
||||
export function useListFiltersForField<Obj, Field extends keyof Obj>(
|
||||
dataSource: Obj[],
|
||||
field: Field): ColumnFilterItem[] {
|
||||
return React.useMemo(() => {
|
||||
const filters: ColumnFilterItem[] = [];
|
||||
dataSource.forEach((element) => {
|
||||
const value = element[field];
|
||||
if (typeof value === "string" && value !== "") {
|
||||
// Make sure it's not already in the filters
|
||||
if (filters.find((f) => f.value === value)) {
|
||||
return;
|
||||
}
|
||||
filters.push({
|
||||
text: value,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Sort the filters
|
||||
filters.sort((a, b) => {
|
||||
if (typeof a.text !== "string" || typeof b.text !== "string") {
|
||||
return 0;
|
||||
}
|
||||
return a.text.localeCompare(b.text);
|
||||
});
|
||||
filters.push({
|
||||
text: "<empty>",
|
||||
value: "",
|
||||
});
|
||||
return filters;
|
||||
}, [dataSource, field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of filter values for a given field based on the provided filters.
|
||||
* @param filters An array of `CrudFilter` objects that define the filtering criteria.
|
||||
|
||||
@@ -7,62 +7,6 @@ interface TypedCrudSort<Obj> {
|
||||
order: "asc" | "desc";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sorting function that can be used to sort an array of objects based on the provided sorters.
|
||||
* @param sorters An array of `CrudSort` objects that define the sorting criteria.
|
||||
* @returns A sorting function that can be used to sort an array of objects based on the provided sorters.
|
||||
*/
|
||||
export function genericSorter<Obj>(sorters: TypedCrudSort<Obj>[]) {
|
||||
return (a: Obj, b: Obj) => {
|
||||
for (const sorter of sorters) {
|
||||
const aValue = a[sorter.field];
|
||||
const bValue = b[sorter.field];
|
||||
if (aValue === bValue) {
|
||||
continue;
|
||||
}
|
||||
// Send empty fields to the bottom
|
||||
if (aValue === undefined || aValue === null) {
|
||||
return 1;
|
||||
}
|
||||
if (bValue === undefined || bValue === null) {
|
||||
return -1;
|
||||
}
|
||||
// Perform sorting based on type of the fields
|
||||
if (typeof aValue === "string" && typeof bValue === "string") {
|
||||
// Try to sort them as dates if possible
|
||||
const aDate = new Date(aValue);
|
||||
const bDate = new Date(bValue);
|
||||
if (!isNaN(aDate.getTime()) && !isNaN(bDate.getTime())) {
|
||||
return sorter.order === "asc"
|
||||
? aDate.getTime() - bDate.getTime()
|
||||
: bDate.getTime() - aDate.getTime();
|
||||
}
|
||||
|
||||
return sorter.order === "asc"
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
}
|
||||
if (typeof aValue === "number" && typeof bValue === "number") {
|
||||
return sorter.order === "asc" ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
if (typeof aValue === "boolean" && typeof bValue === "boolean") {
|
||||
return sorter.order === "asc" ? +aValue - +bValue : +bValue - +aValue;
|
||||
}
|
||||
if (aValue instanceof Date && bValue instanceof Date) {
|
||||
return sorter.order === "asc"
|
||||
? aValue.getTime() - bValue.getTime()
|
||||
: bValue.getTime() - aValue.getTime();
|
||||
}
|
||||
if (typeof aValue === "object" && typeof bValue === "object") {
|
||||
return sorter.order === "asc"
|
||||
? JSON.stringify(aValue).localeCompare(JSON.stringify(bValue))
|
||||
: JSON.stringify(bValue).localeCompare(JSON.stringify(aValue));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sort order for a given field based on the provided sorters.
|
||||
* @param sorters An array of `CrudSort` objects that define the sorting criteria.
|
||||
|
||||
Reference in New Issue
Block a user