Add multi-columns rows sorting with customizable comparison function for each column.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Madeorsk 2024-07-27 13:45:25 +02:00
parent 5ac1e4e7b8
commit 2513e711b1
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
3 changed files with 75 additions and 11 deletions

View file

@ -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";

View file

@ -25,7 +25,7 @@ export function createColumns<K extends ColumnKey>(...columns: [K, Column][]): C
/**
* Smartable column definition.
*/
export interface Column
export interface Column<T = any>
{
/**
* 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;
}
/**

View file

@ -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<CK extends ColumnKey>({columns}: {columns: Colum
export function TableBody<CK extends ColumnKey>()
{
// Get data from table.
const {data} = useTable();
const {data, columns, columnsSortState} = useTable<CK>();
// Get current data state from the async table value.
const {currentDataState} = useAsyncManager(data);
const {currentDataState} = useAsyncManager<CK>(data);
// Memorize sorted rows.
const sortedRows = useMemo(() => (
sortRows<CK>(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.
<RowInstance row={rowData} />
))
@ -69,3 +74,55 @@ export function TableBody<CK extends ColumnKey>()
)
);
}
/**
* 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<CK extends ColumnKey>(rows: CurrentRowData<CK>[], columns: Columns<CK>, columnsSortState: Record<CK, SortState>): CurrentRowData<CK>[]
{
// 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));
}
}