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 { 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({

View File

@@ -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,
};
},

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 { 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>