Reorganize columns sorting and filter systems.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Madeorsk 2024-07-28 14:36:36 +02:00
parent 519facc608
commit 71b10a3c89
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
9 changed files with 146 additions and 137 deletions

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import {ColumnKey} from "./Column"; import {ColumnKey} from "./Column";
import {normalizeRowDefinition, RowData, RowDefinition} from "./Row"; import {normalizeRowDefinition, RowData, RowDefinition} from "./Row";
import {CellDefinition} from "./Cell"; import {CellDefinition} from "./Cell";

View file

@ -1,6 +1,7 @@
import React, {useCallback, useContext} from "react"; import React, {useCallback, useContext} from "react";
import {Smartable, useTable} from "./Smartable"; import {Smartable, useTable} from "./Smartable";
import {ColumnFilter} from "./Columns/ColumnFilter"; import {ColumnFilter} from "./Columns/ColumnFilter";
import {SortState} from "./Sort";
/** /**
* Basic column key type. * Basic column key type.
@ -61,31 +62,6 @@ export function createColumn<K extends ColumnKey>(key: K, column: Column): [K, C
return [key, column]; return [key, column];
} }
/**
* Column sort type.
*/
export enum SortType
{
ASC = "asc",
DESC = "desc",
}
/**
* Column sort state.
*/
export interface SortState
{
/**
* Sort type (ascending or descending).
*/
type: SortType;
/**
* Sort order.
*/
order: number;
}
/** /**
* Table column context data. * Table column context data.
*/ */
@ -128,58 +104,6 @@ export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): Colu
return useContext(ColumnContext); return useContext(ColumnContext);
} }
/**
* Hook to get current column filter state.
*/
export function useFilterState<T = any>(initialValue: T): [T, (newFilterState: T) => void]
{
// Get current column data.
const column = useColumn();
// Return filter state array from current column data.
return [column.filterState ?? initialValue, column.setFilterState];
}
/**
* Default column heading component.
*/
export function ColumnHeading()
{
// Get current column data.
const {key, column, sortState} = useColumn();
// Get column sort state setter.
const {setColumnSortState} = useTable();
// Initialize handle click function.
const handleClick = useCallback((event: React.MouseEvent) => {
if (event.button == 0)
{ // Normal click (usually left click).
// Toggle sort type.
setColumnSortState(key, sortState?.type == SortType.ASC ? SortType.DESC : SortType.ASC);
}
else if (event.button == 2 || event.button == 1)
{ // Alt click (usually right or middle click).
// Reset sort type.
setColumnSortState(key, null);
}
}, [key, sortState, setColumnSortState]);
// Disable context menu function.
const disableContextMenu = useCallback((event: React.MouseEvent) => {
event.preventDefault();
return false;
}, []);
return (
<th className={sortState?.type ?? undefined} data-sort-order={sortState?.order ?? undefined}
onMouseDown={handleClick} onContextMenu={disableContextMenu}>
{column.title}
{sortState?.order && <span className={"order"}>{sortState.order}</span>}
</th>
);
}
/** /**
* Auto column context provider from table context. * Auto column context provider from table context.
*/ */

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import {useColumn} from "../Column";
/** /**
* Column filter definition. * Column filter definition.
@ -18,3 +19,14 @@ export interface ColumnFilter<T = any, FilterState = any>
*/ */
filter: (data: T, filterState: FilterState) => boolean; filter: (data: T, filterState: FilterState) => boolean;
} }
/**
* Hook to get current column filter state.
*/
export function useFilterState<T = any>(initialValue: T): [T, (newFilterState: T) => void]
{
// Get current column data.
const column = useColumn();
// Return filter state array from current column data.
return [column.filterState ?? initialValue, column.setFilterState];
}

View file

@ -0,0 +1,45 @@
import React, {useCallback} from "react";
import {useColumn} from "../Column";
import {useTable} from "../Smartable";
import {SortType} from "../Sort";
/**
* Default column heading component.
*/
export function ColumnHeading()
{
// Get current column data.
const {key, column, sortState} = useColumn();
// Get column sort state setter.
const {setColumnSortState} = useTable();
// Initialize handle click function.
const handleClick = useCallback((event: React.MouseEvent) => {
if (event.button == 0)
{ // Normal click (usually left click).
// Toggle sort type.
setColumnSortState(key, sortState?.type == SortType.ASC ? SortType.DESC : SortType.ASC);
}
else if (event.button == 2 || event.button == 1)
{ // Alt click (usually right or middle click).
// Reset sort type.
setColumnSortState(key, null);
}
}, [key, sortState, setColumnSortState]);
// Disable context menu function.
const disableContextMenu = useCallback((event: React.MouseEvent) => {
event.preventDefault();
return false;
}, []);
return (
<th className={sortState?.type ?? undefined} data-sort-order={sortState?.order ?? undefined}
onMouseDown={handleClick} onContextMenu={disableContextMenu}>
{column.title}
{sortState?.order && <span className={"order"}>{sortState.order}</span>}
</th>
);
}

View file

@ -1,6 +1,5 @@
import React, {useCallback} from "react"; import React, {useCallback} from "react";
import {ColumnFilter} from "../Columns/ColumnFilter"; import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter";
import {useFilterState} from "../Column";
/** /**
* Filter value regex. * Filter value regex.

View file

@ -1,6 +1,5 @@
import React, {useCallback} from "react"; import React, {useCallback} from "react";
import {ColumnFilter} from "../Columns/ColumnFilter"; import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter";
import {useFilterState} from "../Column";
import {normalizeString} from "@kernelui/core"; import {normalizeString} from "@kernelui/core";
/** /**

View file

@ -1,8 +1,10 @@
import React, {useMemo} from "react"; import React, {useMemo} from "react";
import {AutoColumnContextProvider, Column, ColumnHeading, ColumnKey, Columns, SortState, SortType} from "./Column"; import {AutoColumnContextProvider, Column, ColumnKey, Columns} from "./Column";
import {SmartableProperties, useTable} from "./Smartable"; import {SmartableProperties, useTable} from "./Smartable";
import {RowInstance, RowLoader} from "./Row"; import {RowInstance, RowLoader} from "./Row";
import {CurrentRowData, useAsyncManager} from "./AsyncManager"; import {useAsyncManager} from "./AsyncManager";
import {ColumnHeading} from "./Columns/ColumnHeading";
import {sortRows} from "./Sort";
/** /**
* Smartable instance component properties. * Smartable instance component properties.
@ -98,55 +100,3 @@ 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: Partial<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));
}
}

View file

@ -1,8 +1,9 @@
import React, {useCallback, useContext, useMemo, useState} from "react"; import React, {useCallback, useContext, useMemo, useState} from "react";
import {Instance} from "./Instance"; import {Instance} from "./Instance";
import {ColumnKey, Columns, SortState, SortType} from "./Column"; import {ColumnKey, Columns} from "./Column";
import {RowDefinition} from "./Row"; import {RowDefinition} from "./Row";
import {Promisable} from "@kernelui/core"; import {Promisable} from "@kernelui/core";
import {SortState, SortType} from "./Sort";
/** /**
* Smartable data type. * Smartable data type.

79
src/Smartable/Sort.tsx Normal file
View file

@ -0,0 +1,79 @@
import {ColumnKey, Columns} from "./Column";
import {CurrentRowData} from "./AsyncManager";
/**
* Column sort type.
*/
export enum SortType
{
ASC = "asc",
DESC = "desc",
}
/**
* Column sort state.
*/
export interface SortState
{
/**
* Sort type (ascending or descending).
*/
type: SortType;
/**
* Sort order.
*/
order: number;
}
/**
* 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: Partial<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));
}
}