From 2513e711b12a9dce3577019e69e8a6c75c302a8b Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sat, 27 Jul 2024 13:45:25 +0200 Subject: [PATCH] Add multi-columns rows sorting with customizable comparison function for each column. --- src/Smartable/AsyncManager.tsx | 6 +-- src/Smartable/Column.tsx | 9 ++++- src/Smartable/Instance.tsx | 71 ++++++++++++++++++++++++++++++---- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Smartable/AsyncManager.tsx b/src/Smartable/AsyncManager.tsx index d6b3311..947a5fb 100644 --- a/src/Smartable/AsyncManager.tsx +++ b/src/Smartable/AsyncManager.tsx @@ -1,6 +1,6 @@ -import React, {startTransition, useEffect, useRef, useState} from "react"; +import React, { useEffect, useRef, useState} from "react"; import {ColumnKey} from "./Column"; -import {normalizeRowDefinition, RowCells, RowData, RowDefinition} from "./Row"; +import {normalizeRowDefinition, RowData, RowDefinition} from "./Row"; import {CellDefinition} from "./Cell"; import {SmartableData} from "./Smartable"; import {Modify, Promisable} from "@kernelui/core"; @@ -84,7 +84,7 @@ class AsyncManager */ protected promisedRowsCells: Partial>>[] = []; - + /** * Rows data. * @protected diff --git a/src/Smartable/Column.tsx b/src/Smartable/Column.tsx index 3634b50..d1ba4fc 100644 --- a/src/Smartable/Column.tsx +++ b/src/Smartable/Column.tsx @@ -25,7 +25,7 @@ export function createColumns(...columns: [K, Column][]): C /** * Smartable column definition. */ -export interface Column +export interface Column { /** * Column title element. @@ -36,6 +36,13 @@ export interface Column * Column cell default element. */ cellElement?: React.ReactElement; + + /** + * Cells data comparison in the column. + * @param a First data to compare. + * @param b Second data to compare. + */ + compare?: (a: T, b: T) => number; } /** diff --git a/src/Smartable/Instance.tsx b/src/Smartable/Instance.tsx index 1562454..949aa7c 100644 --- a/src/Smartable/Instance.tsx +++ b/src/Smartable/Instance.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import {AutoColumnContextProvider, ColumnHeading, ColumnKey, Columns} from "./Column"; +import React, {useMemo} from "react"; +import {AutoColumnContextProvider, ColumnHeading, ColumnKey, Columns, SortState, SortType} from "./Column"; import {SmartableProperties, useTable} from "./Smartable"; import {RowInstance, RowLoader} from "./Row"; -import {useAsyncManager} from "./AsyncManager"; +import {CurrentRowData, useAsyncManager} from "./AsyncManager"; /** * Smartable instance component properties. @@ -53,14 +53,19 @@ export function ColumnsHeadings({columns}: {columns: Colum export function TableBody() { // Get data from table. - const {data} = useTable(); + const {data, columns, columnsSortState} = useTable(); // Get current data state from the async table value. - const {currentDataState} = useAsyncManager(data); + const {currentDataState} = useAsyncManager(data); + + // Memorize sorted rows. + const sortedRows = useMemo(() => ( + sortRows(currentDataState.rows, columns, columnsSortState) + ), [currentDataState.rows, columns, columnsSortState]); return ( - Array.isArray(currentDataState?.rows) ? ( - currentDataState.rows.map((rowData, index) => ( + sortedRows ? ( + sortedRows.map((rowData, index) => ( // Rendering each row from its definition. )) @@ -69,3 +74,55 @@ export function TableBody() ) ); } + +/** + * Sort the given rows from the columns sort state. + * @param rows Rows to sort. + * @param columns Columns definition. + * @param columnsSortState Columns current sort state. + */ +export function sortRows(rows: CurrentRowData[], columns: Columns, columnsSortState: Record): CurrentRowData[] +{ + // Normalize value to undefined when rows are not loaded. + if (!Array.isArray(rows)) return undefined; + + // Get the sort column states following their priority order. + const orderedSortColumns = (Object.entries(columnsSortState) as [CK, SortState][]).sort((a, b) => ( + a[1].order - b[1].order + )); + + // Sort the given rows. + return rows.toSorted((a, b) => { + for (const [columnKey, columnSort] of orderedSortColumns) + { + // Get the current comparison result for the given cells' data. + const comparison = + (columnSort.type == SortType.DESC ? 1 : -1) * + (columns[columnKey]?.compare ?? genericCompare)(a.cells[columnKey]?.data, b.cells[columnKey]?.data); + + if (comparison != 0) + // If an order can be determined from the current comparison, returning it. + return comparison; + } + + // The comparisons always returned 0. + return 0; + }); +} + +/** + * Generic comparison function for table data. + * @param a First data to compare. + * @param b Second data to compare. + */ +export function genericCompare(a: any, b: any): number +{ + if (typeof a == "number" && typeof a == typeof b) + { // Compare numbers. + return b - a; + } + else + { // Compare data as strings. + return String(b).localeCompare(String(a)); + } +}