Client: Now uses server side sorting

Still some stuff left to do #98
This commit is contained in:
Donkie
2023-09-07 21:24:40 +02:00
parent 826fde6967
commit f62f9d899b
9 changed files with 387 additions and 323 deletions

View File

@@ -1,7 +1,7 @@
import { Col, Row, Table } from "antd"; import { Col, Row, Table } from "antd";
import { ColumnProps as AntdColumnProps } from "antd/es/table"; import { ColumnProps as AntdColumnProps } from "antd/es/table";
import { ColumnFilterItem } from "antd/es/table/interface"; 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 { TableState } from "../utils/saveload";
import { getSortOrderForField, typeSorters } from "../utils/sorting"; import { getSortOrderForField, typeSorters } from "../utils/sorting";
import { NumberFieldUnit } from "./numberField"; import { NumberFieldUnit } from "./numberField";
@@ -12,12 +12,14 @@ import Icon from "@ant-design/icons";
import { ReactComponent as SpoolIcon } from "../icon_spool.svg"; import { ReactComponent as SpoolIcon } from "../icon_spool.svg";
import { useTranslate } from "@refinedev/core"; import { useTranslate } from "@refinedev/core";
import { enrichText } from "../utils/parsing"; import { enrichText } from "../utils/parsing";
import { UseQueryResult } from "@tanstack/react-query";
dayjs.extend(utc); dayjs.extend(utc);
interface BaseColumnProps<Obj> { interface BaseColumnProps<Obj> {
id: keyof Obj & string; id: keyof Obj & string;
i18ncat: string; i18ncat?: string;
i18nkey?: string;
dataSource: Obj[]; dataSource: Obj[];
tableState: TableState; tableState: TableState;
} }
@@ -25,6 +27,7 @@ interface BaseColumnProps<Obj> {
interface FilteredColumnProps { interface FilteredColumnProps {
filters?: ColumnFilterItem[]; filters?: ColumnFilterItem[];
filteredValue?: string[]; filteredValue?: string[];
allowMultipleFilters?: boolean;
} }
interface CustomColumnProps<Obj> { interface CustomColumnProps<Obj> {
@@ -45,9 +48,10 @@ function Column<Obj>(props: BaseColumnProps<Obj> & FilteredColumnProps & CustomC
const typedSorters = typeSorters<Obj>(props.tableState.sorters); const typedSorters = typeSorters<Obj>(props.tableState.sorters);
const columnProps: AntdColumnProps<Obj> = { const columnProps: AntdColumnProps<Obj> = {
dataIndex: props.id, dataIndex: props.id,
title: t(`${props.i18ncat}.fields.${props.id}`), title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
sorter: true, sorter: true,
sortOrder: getSortOrderForField(typedSorters, props.id), sortOrder: getSortOrderForField(typedSorters, props.id),
filterMultiple: props.allowMultipleFilters ?? true,
}; };
if (props.filters && props.filteredValue) { if (props.filters && props.filteredValue) {
columnProps.filters = props.filters; columnProps.filters = props.filters;
@@ -75,10 +79,29 @@ export function RichColumn<Obj>(props: BaseColumnProps<Obj>) {
}); });
} }
export function FilteredColumn<Obj>(props: BaseColumnProps<Obj>) { interface FilteredQueryColumnProps<Obj> extends BaseColumnProps<Obj> {
const typedFilters = typeFilters<Obj>(props.tableState.filters); 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); const filteredValue = getFiltersForField(typedFilters, props.id);
return Column({ ...props, filters, filteredValue }); 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; color: (record: Obj) => string | undefined;
} }
export function SpoolIconColumn<Obj>(props: SpoolIconColumnProps<Obj>) { 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); const filteredValue = getFiltersForField(typedFilters, props.id);
return Column({ return Column({

View File

@@ -16,7 +16,8 @@ const dataProvider = (
getList: async ({ resource, meta, pagination, sorters, filters }) => { getList: async ({ resource, meta, pagination, sorters, filters }) => {
const url = `${apiUrl}/${resource}`; 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"; const requestMethod = (method as MethodTypes) ?? "get";
if (pagination && pagination.mode == "server") { if (pagination && pagination.mode == "server") {
@@ -28,22 +29,60 @@ const dataProvider = (
if (sorters && sorters.length > 0) { if (sorters && sorters.length > 0) {
queryParams["sort"] = sorters.map((sort) => { queryParams["sort"] = sorters.map((sort) => {
const field = sort.field.replace("_", ".") const field = sort.field
return `${field}:${sort.order}`; return `${field}:${sort.order}`;
}).join(",") }).join(",")
} }
if (filters && filters.length > 0) { 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 => { filters.forEach(filter => {
if (!("field" in filter)) { if (!("field" in filter)) {
throw Error("Filter must be a LogicalFilter.") throw Error("Filter must be a LogicalFilter.")
} }
const field = filter.field.replace(".", "_") const field = filter.field
queryParams[field] = filter.value 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}`, `${url}`,
{ {
headers: headersFromMeta, headers: headersFromMeta,
@@ -51,10 +90,11 @@ const dataProvider = (
}, },
); );
// console.log(url, requestMethod, queryParams, data, headers)
return { return {
data, data,
total: 100, total: parseInt(headers["x-total-count"]) ?? 100,
//TODO: total: data.length,
}; };
}, },

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

View File

@@ -1,12 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd"; import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd";
import { ISpool } from "../pages/spools/model"; import { ISpool } from "../pages/spools/model";
import { FilteredColumn, SortedColumn, SpoolIconColumn } from "./column"; import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "./column";
import { TableState } from "../utils/saveload"; import { TableState } from "../utils/saveload";
import { useTable } from "@refinedev/antd"; import { useTable } from "@refinedev/antd";
import { genericSorter, typeSorters } from "../utils/sorting";
import { genericFilterer, typeFilters } from "../utils/filtering";
import { t } from "i18next"; import { t } from "i18next";
import { useSpoolmanFilamentFullNames, useSpoolmanMaterials } from "./otherModels";
interface Props { interface Props {
visible: boolean; visible: boolean;
@@ -16,8 +15,8 @@ interface Props {
} }
interface ISpoolCollapsed extends ISpool { interface ISpoolCollapsed extends ISpool {
filament_name: string; "filament.name": string;
material?: string; "filament.material"?: string;
} }
const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onContinue }) => { const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onContinue }) => {
@@ -64,28 +63,17 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
} }
return { return {
...element, ...element,
filament_name, "filament.name": filament_name,
material: element.filament.material, "filament.material": element.filament.material,
}; };
}), }),
[tableProps.dataSource] [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 // Function to add/remove all filtered items from selected items
const selectUnselectFiltered = (select: boolean) => { const selectUnselectFiltered = (select: boolean) => {
setSelectedItems((prevSelected) => { 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; 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 // 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 ( return (
<Modal <Modal
@@ -123,7 +111,7 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
<Table <Table
{...tableProps} {...tableProps}
rowKey="id" rowKey="id"
dataSource={filteredDataSource} dataSource={dataSource}
pagination={false} pagination={false}
scroll={{ x: "max-content", y: 200 }} scroll={{ x: "max-content", y: 200 }}
> >
@@ -140,17 +128,19 @@ const SpoolSelectModal: React.FC<Props> = ({ visible, description, onCancel, onC
tableState, tableState,
})} })}
{SpoolIconColumn({ {SpoolIconColumn({
id: "filament_name", id: "filament.name",
i18ncat: "spool", i18ncat: "spool",
color: (record: ISpoolCollapsed) => record.filament.color_hex, color: (record: ISpoolCollapsed) => record.filament.color_hex,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanFilamentFullNames(),
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "material", id: "filament.material",
i18ncat: "spool", i18nkey: "spool.fields.material",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanMaterials(),
})} })}
</Table> </Table>
<Row> <Row>

View File

@@ -5,54 +5,59 @@ import { Table, Space, Button, Dropdown } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { IFilament } from "./model"; import { IFilament } from "./model";
import { genericSorter, typeSorters } from "../../utils/sorting";
import { genericFilterer, typeFilters } from "../../utils/filtering";
import { EditOutlined, FilterOutlined } from "@ant-design/icons"; import { EditOutlined, FilterOutlined } from "@ant-design/icons";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { import {
DateColumn, DateColumn,
FilteredColumn, FilteredQueryColumn,
NumberColumn, NumberColumn,
RichColumn, RichColumn,
SortedColumn, SortedColumn,
SpoolIconColumn, SpoolIconColumn,
} from "../../components/column"; } from "../../components/column";
import {
useSpoolmanArticleNumbers,
useSpoolmanFilamentNames,
useSpoolmanMaterials,
useSpoolmanVendors,
} from "../../components/otherModels";
dayjs.extend(utc); dayjs.extend(utc);
interface IFilamentCollapsed extends Omit<IFilament, "vendor"> { interface IFilamentCollapsed extends Omit<IFilament, "vendor"> {
vendor_name: string | null; "vendor.name": string | null;
} }
const namespace = "filamentList-v2";
export const FilamentList: React.FC<IResourceComponentsProps> = () => { export const FilamentList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
// Load initial state // Load initial state
const initialState = useInitialTableState("filamentList"); const initialState = useInitialTableState(namespace);
// Fetch data from the API // Fetch data from the API
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } = const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IFilament>({
useTable<IFilament>({ syncWithLocation: false,
syncWithLocation: false, pagination: {
pagination: { mode: "server",
mode: "off", // Perform pagination in antd's Table instead. Otherwise client-side sorting/filtering doesn't work. current: initialState.pagination.current,
current: initialState.pagination.current, pageSize: initialState.pagination.pageSize,
pageSize: initialState.pagination.pageSize, },
}, sorters: {
sorters: { mode: "server",
mode: "off", // Disable server-side sorting initial: initialState.sorters,
initial: initialState.sorters, },
}, filters: {
filters: { mode: "server",
mode: "off", // Disable server-side filtering initial: initialState.filters,
initial: initialState.filters, },
}, });
});
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof IFilamentCollapsed & string)[] = [ const allColumns: (keyof IFilamentCollapsed & string)[] = [
"id", "id",
"vendor_name", "vendor.name",
"name", "name",
"material", "material",
"price", "price",
@@ -72,10 +77,6 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns); 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 // Store state in local storage
const tableState: TableState = { const tableState: TableState = {
sorters, sorters,
@@ -83,7 +84,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
pagination: { current, pageSize }, pagination: { current, pageSize },
showColumns, showColumns,
}; };
useStoreInitialState("filamentList", tableState); useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list and add a filament_name field // Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: IFilamentCollapsed[] = React.useMemo( const dataSource: IFilamentCollapsed[] = React.useMemo(
@@ -95,17 +96,14 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
} else { } else {
vendor_name = null; vendor_name = null;
} }
return { ...element, vendor_name }; return { ...element, "vendor.name": vendor_name };
}), }),
[tableProps.dataSource] [tableProps.dataSource]
); );
// Filter and sort the dataSource if (tableProps.pagination) {
const filteredDataSource = React.useMemo(() => { tableProps.pagination.showSizeChanger = true;
const filtered = dataSource.filter(genericFilterer(typedFilters)); }
filtered.sort(genericSorter(typedSorters));
return filtered;
}, [dataSource, typedFilters, typedSorters]);
return ( return (
<List <List
@@ -148,31 +146,20 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
</> </>
)} )}
> >
<Table <Table {...tableProps} dataSource={dataSource} rowKey="id">
{...tableProps}
dataSource={filteredDataSource}
pagination={{
showSizeChanger: true,
current: current,
pageSize: pageSize,
onChange: (page, pageSize) => {
setCurrent(page);
setPageSize(pageSize);
},
}}
rowKey="id"
>
{SortedColumn({ {SortedColumn({
id: "id", id: "id",
i18ncat: "filament", i18ncat: "filament",
dataSource, dataSource,
tableState, tableState,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "vendor_name", id: "vendor.name",
i18ncat: "filament", i18nkey: "filament.fields.vendor_name",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanVendors(),
allowMultipleFilters: false,
})} })}
{SpoolIconColumn({ {SpoolIconColumn({
id: "name", id: "name",
@@ -180,12 +167,16 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
color: (record: IFilamentCollapsed) => record.color_hex, color: (record: IFilamentCollapsed) => record.color_hex,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanFilamentNames(),
allowMultipleFilters: false,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "material", id: "material",
i18ncat: "filament", i18ncat: "filament",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanMaterials(),
allowMultipleFilters: false,
})} })}
{SortedColumn({ {SortedColumn({
id: "price", id: "price",
@@ -225,11 +216,13 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
dataSource, dataSource,
tableState, tableState,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "article_number", id: "article_number",
i18ncat: "filament", i18ncat: "filament",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanArticleNumbers(),
allowMultipleFilters: false,
})} })}
{NumberColumn({ {NumberColumn({
id: "settings_extruder_temp", id: "settings_extruder_temp",

View File

@@ -4,14 +4,12 @@ import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/
import { Table, Space, Button, Dropdown, Modal } from "antd"; import { Table, Space, Button, Dropdown, Modal } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { genericSorter, typeSorters } from "../../utils/sorting";
import { genericFilterer, typeFilters } from "../../utils/filtering";
import { ISpool } from "./model"; import { ISpool } from "./model";
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, FilterOutlined, InboxOutlined, ToTopOutlined } from "@ant-design/icons"; import { EditOutlined, FilterOutlined, InboxOutlined, ToTopOutlined } from "@ant-design/icons";
import { import {
DateColumn, DateColumn,
FilteredColumn, FilteredQueryColumn,
NumberColumn, NumberColumn,
RichColumn, RichColumn,
SortedColumn, SortedColumn,
@@ -19,55 +17,62 @@ import {
} from "../../components/column"; } from "../../components/column";
import { setSpoolArchived } from "./functions"; import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog"; import SelectAndPrint from "../../components/selectAndPrintDialog";
import {
useSpoolmanFilamentFullNames,
useSpoolmanLocations,
useSpoolmanLotNumbers,
useSpoolmanMaterials,
} from "../../components/otherModels";
dayjs.extend(utc); dayjs.extend(utc);
const { confirm } = Modal; const { confirm } = Modal;
interface ISpoolCollapsed extends ISpool { interface ISpoolCollapsed extends ISpool {
filament_name: string; "filament.name": string;
filament_material?: string; "filament.material"?: string;
} }
const namespace = "spoolList-v2";
export const SpoolList: React.FC<IResourceComponentsProps> = () => { export const SpoolList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
const invalidate = useInvalidate(); const invalidate = useInvalidate();
// Load initial state // Load initial state
const initialState = useInitialTableState("spoolList"); const initialState = useInitialTableState(namespace);
// State for the switch to show archived spools // State for the switch to show archived spools
const [showArchived, setShowArchived] = useSavedState("spoolList-showArchived", false); const [showArchived, setShowArchived] = useSavedState("spoolList-showArchived", false);
// Fetch data from the API // Fetch data from the API
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } = const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<ISpool>({
useTable<ISpool>({ meta: {
meta: { queryParams: {
queryParams: { ["allow_archived"]: showArchived,
["allow_archived"]: showArchived,
},
}, },
syncWithLocation: false, },
pagination: { syncWithLocation: false,
mode: "server", pagination: {
current: initialState.pagination.current, mode: "server",
pageSize: initialState.pagination.pageSize, current: initialState.pagination.current,
}, pageSize: initialState.pagination.pageSize,
sorters: { },
mode: "server", sorters: {
initial: initialState.sorters, mode: "server",
}, initial: initialState.sorters,
filters: { },
mode: "server", filters: {
initial: initialState.filters, mode: "server",
}, initial: initialState.filters,
}); },
});
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof ISpoolCollapsed & string)[] = [ const allColumns: (keyof ISpoolCollapsed & string)[] = [
"id", "id",
"filament_name", "filament.name",
"filament_material", "filament.material",
"used_weight", "used_weight",
"remaining_weight", "remaining_weight",
"used_length", "used_length",
@@ -91,7 +96,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
pagination: { current, pageSize }, pagination: { current, pageSize },
showColumns, showColumns,
}; };
useStoreInitialState("spoolList", tableState); useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list and add a filament_name field // Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: ISpoolCollapsed[] = React.useMemo( const dataSource: ISpoolCollapsed[] = React.useMemo(
@@ -105,8 +110,8 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
} }
return { return {
...element, ...element,
filament_name, "filament.name": filament_name,
filament_material: element.filament.material, "filament.material": element.filament.material,
}; };
}), }),
[tableProps.dataSource] [tableProps.dataSource]
@@ -143,6 +148,10 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
} }
}; };
if (tableProps.pagination) {
tableProps.pagination.showSizeChanger = true;
}
return ( return (
<List <List
headerButtons={({ defaultButtons }) => ( headerButtons={({ defaultButtons }) => (
@@ -219,17 +228,21 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
tableState, tableState,
})} })}
{SpoolIconColumn({ {SpoolIconColumn({
id: "filament_name", id: "filament.name",
i18ncat: "spool", i18nkey: "spool.fields.filament_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex, color: (record: ISpoolCollapsed) => record.filament.color_hex,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanFilamentFullNames(),
allowMultipleFilters: false,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "filament_material", id: "filament.material",
i18ncat: "spool", i18nkey: "spool.fields.material",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanMaterials(),
allowMultipleFilters: false,
})} })}
{NumberColumn({ {NumberColumn({
id: "used_weight", id: "used_weight",
@@ -265,17 +278,21 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
dataSource, dataSource,
tableState, tableState,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "location", id: "location",
i18ncat: "spool", i18ncat: "spool",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanLocations(),
allowMultipleFilters: false,
})} })}
{FilteredColumn({ {FilteredQueryColumn({
id: "lot_nr", id: "lot_nr",
i18ncat: "spool", i18ncat: "spool",
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanLotNumbers(),
allowMultipleFilters: false,
})} })}
{DateColumn({ {DateColumn({
id: "first_used", id: "first_used",

View File

@@ -4,8 +4,6 @@ import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/
import { Table, Space, Button, Dropdown } from "antd"; import { Table, Space, Button, Dropdown } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { genericSorter, typeSorters } from "../../utils/sorting";
import { genericFilterer, typeFilters } from "../../utils/filtering";
import { IVendor } from "./model"; import { IVendor } from "./model";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, FilterOutlined } from "@ant-design/icons"; import { EditOutlined, FilterOutlined } from "@ant-design/icons";
@@ -13,39 +11,36 @@ import { DateColumn, RichColumn, SortedColumn } from "../../components/column";
dayjs.extend(utc); dayjs.extend(utc);
const namespace = "vendorList-v2";
export const VendorList: React.FC<IResourceComponentsProps> = () => { export const VendorList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
// Load initial state // Load initial state
const initialState = useInitialTableState("vendorList"); const initialState = useInitialTableState(namespace);
// Fetch data from the API // Fetch data from the API
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent, setPageSize } = const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IVendor>({
useTable<IVendor>({ syncWithLocation: false,
syncWithLocation: false, pagination: {
pagination: { mode: "server",
mode: "off", // Perform pagination in antd's Table instead. Otherwise client-side sorting/filtering doesn't work. current: initialState.pagination.current,
current: initialState.pagination.current, pageSize: initialState.pagination.pageSize,
pageSize: initialState.pagination.pageSize, },
}, sorters: {
sorters: { mode: "server",
mode: "off", // Disable server-side sorting initial: initialState.sorters,
initial: initialState.sorters, },
}, filters: {
filters: { mode: "server",
mode: "off", // Disable server-side filtering initial: initialState.filters,
initial: initialState.filters, },
}, });
});
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"]; const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"];
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? allColumns); 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 // Store state in local storage
const tableState: TableState = { const tableState: TableState = {
sorters, sorters,
@@ -53,7 +48,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
pagination: { current, pageSize }, pagination: { current, pageSize },
showColumns, showColumns,
}; };
useStoreInitialState("vendorList", tableState); useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list // Collapse the dataSource to a mutable list
const dataSource: IVendor[] = React.useMemo( const dataSource: IVendor[] = React.useMemo(
@@ -61,12 +56,9 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
[tableProps.dataSource] [tableProps.dataSource]
); );
// Filter and sort the dataSource if (tableProps.pagination) {
const filteredDataSource = React.useMemo(() => { tableProps.pagination.showSizeChanger = true;
const filtered = dataSource.filter(genericFilterer(typedFilters)); }
filtered.sort(genericSorter(typedSorters));
return filtered;
}, [dataSource, typedFilters, typedSorters]);
return ( return (
<List <List
@@ -109,20 +101,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
</> </>
)} )}
> >
<Table <Table {...tableProps} dataSource={dataSource} rowKey="id">
{...tableProps}
dataSource={filteredDataSource}
pagination={{
showSizeChanger: true,
current: current,
pageSize: pageSize,
onChange: (page, pageSize) => {
setCurrent(page);
setPageSize(pageSize);
},
}}
rowKey="id"
>
{SortedColumn({ {SortedColumn({
id: "id", id: "id",
i18ncat: "vendor", i18ncat: "vendor",

View File

@@ -1,6 +1,4 @@
import { CrudFilter, CrudOperators } from "@refinedev/core"; import { CrudFilter, CrudOperators } from "@refinedev/core";
import { ColumnFilterItem } from "antd/es/table/interface";
import React from "react";
interface TypedCrudFilter<Obj> { interface TypedCrudFilter<Obj> {
field: keyof Obj; field: keyof Obj;
@@ -12,92 +10,6 @@ export function typeFilters<Obj>(filters: CrudFilter[]): TypedCrudFilter<Obj>[]
return filters as TypedCrudFilter<Obj>[]; // <-- Unsafe cast 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. * 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. * @param filters An array of `CrudFilter` objects that define the filtering criteria.

View File

@@ -7,62 +7,6 @@ interface TypedCrudSort<Obj> {
order: "asc" | "desc"; 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. * 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. * @param sorters An array of `CrudSort` objects that define the sorting criteria.