diff --git a/client/src/components/column.tsx b/client/src/components/column.tsx index e9252a6..04163ed 100644 --- a/client/src/components/column.tsx +++ b/client/src/components/column.tsx @@ -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 { id: keyof Obj & string; - i18ncat: string; + i18ncat?: string; + i18nkey?: string; dataSource: Obj[]; tableState: TableState; } @@ -25,6 +27,7 @@ interface BaseColumnProps { interface FilteredColumnProps { filters?: ColumnFilterItem[]; filteredValue?: string[]; + allowMultipleFilters?: boolean; } interface CustomColumnProps { @@ -45,9 +48,10 @@ function Column(props: BaseColumnProps & FilteredColumnProps & CustomC const typedSorters = typeSorters(props.tableState.sorters); const columnProps: AntdColumnProps = { 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(props: BaseColumnProps) { }); } -export function FilteredColumn(props: BaseColumnProps) { - const typedFilters = typeFilters(props.tableState.filters); +interface FilteredQueryColumnProps extends BaseColumnProps { + filterValueQuery: UseQueryResult; + allowMultipleFilters?: boolean; +} - const filters = useListFiltersForField(props.dataSource, props.id); +export function FilteredQueryColumn(props: FilteredQueryColumnProps) { + const query = props.filterValueQuery; + + let filters: ColumnFilterItem[] = []; + if (query.data) { + filters = query.data.map((item) => { + return { + text: item, + value: item, + }; + }); + } + filters.push({ + text: "", + value: "", + }); + + const typedFilters = typeFilters(props.tableState.filters); const filteredValue = getFiltersForField(typedFilters, props.id); return Column({ ...props, filters, filteredValue }); @@ -127,14 +150,28 @@ export function DateColumn(props: BaseColumnProps) { }); } -interface SpoolIconColumnProps extends BaseColumnProps { +interface SpoolIconColumnProps extends FilteredQueryColumnProps { color: (record: Obj) => string | undefined; } export function SpoolIconColumn(props: SpoolIconColumnProps) { - const typedFilters = typeFilters(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: "", + value: "", + }); + + const typedFilters = typeFilters(props.tableState.filters); const filteredValue = getFiltersForField(typedFilters, props.id); return Column({ diff --git a/client/src/components/dataProvider.ts b/client/src/components/dataProvider.ts index 4e93b26..e532513 100644 --- a/client/src/components/dataProvider.ts +++ b/client/src/components/dataProvider.ts @@ -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 = 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, }; }, diff --git a/client/src/components/otherModels.tsx b/client/src/components/otherModels.tsx new file mode 100644 index 0000000..178fbd0 --- /dev/null +++ b/client/src/components/otherModels.tsx @@ -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({ + 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 ?? ""}`; + } else { + return `${filament.name ?? ""}`; + } + }) + .sort(); + // Remove duplicates + names = [...new Set(names)]; + return names; + }, + }); +} + +export function useSpoolmanFilamentNames() { + const apiEndpoint = import.meta.env.VITE_APIURL; + return useQuery({ + 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 ?? ""; + }) + .sort(); + // Remove duplicates + names = [...new Set(names)]; + return names; + }, + }); +} + +export function useSpoolmanVendors() { + const apiEndpoint = import.meta.env.VITE_APIURL; + return useQuery({ + 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({ + 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({ + 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({ + 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({ + 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(); + }, + }); +} diff --git a/client/src/components/spoolSelectModal.tsx b/client/src/components/spoolSelectModal.tsx index 1f935c5..a944151 100644 --- a/client/src/components/spoolSelectModal.tsx +++ b/client/src/components/spoolSelectModal.tsx @@ -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 = ({ visible, description, onCancel, onContinue }) => { @@ -64,28 +63,17 @@ const SpoolSelectModal: React.FC = ({ 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(sorters); - const typedFilters = typeFilters(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 = ({ 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 ( = ({ visible, description, onCancel, onC @@ -140,17 +128,19 @@ const SpoolSelectModal: React.FC = ({ 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(), })}
diff --git a/client/src/pages/filaments/list.tsx b/client/src/pages/filaments/list.tsx index 65d739c..74893c9 100644 --- a/client/src/pages/filaments/list.tsx +++ b/client/src/pages/filaments/list.tsx @@ -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 { - vendor_name: string | null; + "vendor.name": string | null; } +const namespace = "filamentList-v2"; + export const FilamentList: React.FC = () => { 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({ - 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({ + 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 = () => { const [showColumns, setShowColumns] = React.useState(initialState.showColumns ?? defaultColumns); - // Type the sorters and filters - const typedSorters = typeSorters(sorters); - const typedFilters = typeFilters(filters); - // Store state in local storage const tableState: TableState = { sorters, @@ -83,7 +84,7 @@ export const FilamentList: React.FC = () => { 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 = () => { } 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 ( = () => { )} > - { - setCurrent(page); - setPageSize(pageSize); - }, - }} - 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 = () => { 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 = () => { dataSource, tableState, })} - {FilteredColumn({ + {FilteredQueryColumn({ id: "article_number", i18ncat: "filament", dataSource, tableState, + filterValueQuery: useSpoolmanArticleNumbers(), + allowMultipleFilters: false, })} {NumberColumn({ id: "settings_extruder_temp", diff --git a/client/src/pages/spools/list.tsx b/client/src/pages/spools/list.tsx index 5adb287..5b72bba 100644 --- a/client/src/pages/spools/list.tsx +++ b/client/src/pages/spools/list.tsx @@ -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 = () => { 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({ - meta: { - queryParams: { - ["allow_archived"]: showArchived, - }, + const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable({ + 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 = () => { 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 = () => { } 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 = () => { } }; + if (tableProps.pagination) { + tableProps.pagination.showSizeChanger = true; + } + return ( ( @@ -219,17 +228,21 @@ export const SpoolList: React.FC = () => { 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 = () => { 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", diff --git a/client/src/pages/vendors/list.tsx b/client/src/pages/vendors/list.tsx index 345e5d5..36739da 100644 --- a/client/src/pages/vendors/list.tsx +++ b/client/src/pages/vendors/list.tsx @@ -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 = () => { 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({ - 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({ + 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(initialState.showColumns ?? allColumns); - // Type the sorters and filters - const typedSorters = typeSorters(sorters); - const typedFilters = typeFilters(filters); - // Store state in local storage const tableState: TableState = { sorters, @@ -53,7 +48,7 @@ export const VendorList: React.FC = () => { 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 = () => { [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 ( = () => { )} > -
{ - setCurrent(page); - setPageSize(pageSize); - }, - }} - rowKey="id" - > +
{SortedColumn({ id: "id", i18ncat: "vendor", diff --git a/client/src/utils/filtering.ts b/client/src/utils/filtering.ts index 893139a..1ce7e8b 100644 --- a/client/src/utils/filtering.ts +++ b/client/src/utils/filtering.ts @@ -1,6 +1,4 @@ import { CrudFilter, CrudOperators } from "@refinedev/core"; -import { ColumnFilterItem } from "antd/es/table/interface"; -import React from "react"; interface TypedCrudFilter { field: keyof Obj; @@ -12,92 +10,6 @@ export function typeFilters(filters: CrudFilter[]): TypedCrudFilter[] return filters as TypedCrudFilter[]; // <-- 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(filters: TypedCrudFilter[]) { - 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( - 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: "", - 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. diff --git a/client/src/utils/sorting.ts b/client/src/utils/sorting.ts index 3855d68..c43b525 100644 --- a/client/src/utils/sorting.ts +++ b/client/src/utils/sorting.ts @@ -7,62 +7,6 @@ interface TypedCrudSort { 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(sorters: TypedCrudSort[]) { - 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.