Add columns filters system and default StringFilter dans NumberFilter.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
f25ca0cc2e
commit
519facc608
10 changed files with 292 additions and 26 deletions
|
@ -5,6 +5,8 @@ import {RowDefinition} from "../src/Smartable/Row";
|
|||
import {CellDefinition} from "../src/Smartable/Cell";
|
||||
import {ClickableCell} from "../src/Smartable/Cells/ClickableCell";
|
||||
import {Buttons, Modal, ModalType, useModals} from "@kernelui/core";
|
||||
import {StringFilter} from "../src/Smartable/Filters/StringFilter";
|
||||
import {NumberFilter} from "../src/Smartable/Filters/NumberFilter";
|
||||
|
||||
/**
|
||||
* Some ants names.
|
||||
|
@ -66,9 +68,11 @@ const Smartable = createSmartable({
|
|||
columns: createColumns(
|
||||
createColumn("name", {
|
||||
title: "Name",
|
||||
filter: StringFilter,
|
||||
}),
|
||||
createColumn("quantity", {
|
||||
title: "Quantity",
|
||||
filter: NumberFilter,
|
||||
}),
|
||||
createColumn("unit-price", {
|
||||
title: "Unit price",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, {useCallback, useContext} from "react";
|
||||
import {Smartable, useTable} from "./Smartable";
|
||||
import {ColumnFilter} from "./Columns/ColumnFilter";
|
||||
|
||||
/**
|
||||
* Basic column key type.
|
||||
|
@ -43,6 +44,11 @@ export interface Column<T = any>
|
|||
* @param b Second data to compare.
|
||||
*/
|
||||
compare?: (a: T, b: T) => number;
|
||||
|
||||
/**
|
||||
* Column filter definition.
|
||||
*/
|
||||
filter?: ColumnFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,6 +105,17 @@ export interface ColumnContextData<CK extends ColumnKey>
|
|||
* Column sort state.
|
||||
*/
|
||||
sortState?: SortState;
|
||||
|
||||
/**
|
||||
* Column filter state.
|
||||
*/
|
||||
filterState: any;
|
||||
|
||||
/**
|
||||
* Set current column filter state.
|
||||
* @param filterState New filter state.
|
||||
*/
|
||||
setFilterState: <T = any>(filterState: T) => void;
|
||||
}
|
||||
|
||||
export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined);
|
||||
|
@ -111,6 +128,17 @@ export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): Colu
|
|||
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.
|
||||
*/
|
||||
|
@ -160,6 +188,12 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
|||
// Get table data.
|
||||
const table = useTable();
|
||||
|
||||
// Initialize filterState dispatcher for the current column.
|
||||
const setFilterState = useCallback((filterState: any) => (
|
||||
// Set the filter state for the current column key.
|
||||
table.setColumnFilterState(columnKey, filterState)
|
||||
), [columnKey, table.setColumnFilterState]);
|
||||
|
||||
return (
|
||||
<ColumnContext.Provider value={{
|
||||
key: columnKey,
|
||||
|
@ -167,6 +201,10 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
|||
column: table.columns[columnKey],
|
||||
// Get current column sort state from table data.
|
||||
sortState: table.columnsSortState?.[columnKey],
|
||||
// Get current column filter state from table data.
|
||||
filterState: table.columnsFilterStates?.[columnKey],
|
||||
// Current column filter state dispatcher.
|
||||
setFilterState: setFilterState,
|
||||
}}>
|
||||
{children}
|
||||
</ColumnContext.Provider>
|
||||
|
|
20
src/Smartable/Columns/ColumnFilter.tsx
Normal file
20
src/Smartable/Columns/ColumnFilter.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from "react";
|
||||
|
||||
/**
|
||||
* Column filter definition.
|
||||
*/
|
||||
export interface ColumnFilter<T = any, FilterState = any>
|
||||
{
|
||||
/**
|
||||
* Visual element for the filter.
|
||||
*/
|
||||
element: React.ReactElement;
|
||||
|
||||
/**
|
||||
* Data filter function.
|
||||
* @param data Data to filter
|
||||
* @param filterState Current filter state.
|
||||
* @returns True when data should be kept, false otherwise.
|
||||
*/
|
||||
filter: (data: T, filterState: FilterState) => boolean;
|
||||
}
|
72
src/Smartable/Filters/NumberFilter.tsx
Normal file
72
src/Smartable/Filters/NumberFilter.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import React, {useCallback} from "react";
|
||||
import {ColumnFilter} from "../Columns/ColumnFilter";
|
||||
import {useFilterState} from "../Column";
|
||||
|
||||
/**
|
||||
* Filter value regex.
|
||||
*/
|
||||
export const filterRegex = /^([=!><])?([0-9]+)$/;
|
||||
|
||||
/**
|
||||
* Number column filter.
|
||||
*/
|
||||
export const NumberFilter: ColumnFilter<number, NumberFilterState> = {
|
||||
filter: (data: number, filterState: NumberFilterState) => {
|
||||
// Read filter value.
|
||||
const filterValue = filterRegex.exec(filterState.value);
|
||||
// No valid filter value, allow everything.
|
||||
if (!filterValue?.length) return true;
|
||||
// Get current filter modifier and number.
|
||||
const filterModifier = filterValue[1];
|
||||
const filterNumber = Number.parseFloat(filterValue[2]);
|
||||
// Return result based on modifier.
|
||||
switch (filterModifier)
|
||||
{
|
||||
case ">":
|
||||
return data > filterNumber;
|
||||
case "<":
|
||||
return data < filterNumber;
|
||||
case "!":
|
||||
return data != filterNumber;
|
||||
case "=":
|
||||
default:
|
||||
return data == filterNumber;
|
||||
}
|
||||
},
|
||||
|
||||
element: <NumberFilterComponent />
|
||||
};
|
||||
|
||||
/**
|
||||
* Number filter state.
|
||||
*/
|
||||
export interface NumberFilterState
|
||||
{
|
||||
/**
|
||||
* Filter value.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number filter component.
|
||||
*/
|
||||
export function NumberFilterComponent()
|
||||
{
|
||||
// Initialize number filter state.
|
||||
const [numberFilterState, setNumberFilterState] =
|
||||
useFilterState<NumberFilterState>({ value: "" });
|
||||
|
||||
// Handle filter input change.
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// Save the current filter value.
|
||||
setNumberFilterState({
|
||||
value: event.currentTarget.value,
|
||||
})
|
||||
}, [setNumberFilterState]);
|
||||
|
||||
return (
|
||||
<input type={"text"} pattern={filterRegex.source} size={1}
|
||||
value={numberFilterState.value} onChange={handleChange} />
|
||||
);
|
||||
}
|
61
src/Smartable/Filters/StringFilter.tsx
Normal file
61
src/Smartable/Filters/StringFilter.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, {useCallback} from "react";
|
||||
import {ColumnFilter} from "../Columns/ColumnFilter";
|
||||
import {useFilterState} from "../Column";
|
||||
import {normalizeString} from "@kernelui/core";
|
||||
|
||||
/**
|
||||
* Filter value regex.
|
||||
*/
|
||||
export const filterRegex = /^([=!])?([^=!].+)$/;
|
||||
|
||||
/**
|
||||
* String column filter.
|
||||
*/
|
||||
export const StringFilter: ColumnFilter<string, StringFilterState> = {
|
||||
filter: (data: string, filterState: StringFilterState) => {
|
||||
// Read filter value.
|
||||
const filterValue = filterRegex.exec(filterState.value);
|
||||
// No valid filter value, allow everything.
|
||||
if (!filterValue?.length) return true;
|
||||
// Get current filter result.
|
||||
const filterResult = normalizeString(data).includes(normalizeString(filterValue[2]));
|
||||
// Invert filter result based on filter modifier.
|
||||
return filterValue[1] == "!" ? !filterResult : filterResult;
|
||||
},
|
||||
|
||||
element: <StringFilterComponent />
|
||||
};
|
||||
|
||||
/**
|
||||
* String filter state.
|
||||
*/
|
||||
export interface StringFilterState
|
||||
{
|
||||
/**
|
||||
* Filter value.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* String filter component.
|
||||
*/
|
||||
export function StringFilterComponent()
|
||||
{
|
||||
// Initialize string filter state.
|
||||
const [stringFilterState, setStringFilterState] =
|
||||
useFilterState<StringFilterState>({ value: "" });
|
||||
|
||||
// Handle filter input change.
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// Save the current filter value.
|
||||
setStringFilterState({
|
||||
value: event.currentTarget.value,
|
||||
})
|
||||
}, [setStringFilterState]);
|
||||
|
||||
return (
|
||||
<input type={"text"} pattern={filterRegex.source} size={1}
|
||||
value={stringFilterState.value} onChange={handleChange} />
|
||||
);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useMemo} from "react";
|
||||
import {AutoColumnContextProvider, ColumnHeading, ColumnKey, Columns, SortState, SortType} from "./Column";
|
||||
import {AutoColumnContextProvider, Column, ColumnHeading, ColumnKey, Columns, SortState, SortType} from "./Column";
|
||||
import {SmartableProperties, useTable} from "./Smartable";
|
||||
import {RowInstance, RowLoader} from "./Row";
|
||||
import {CurrentRowData, useAsyncManager} from "./AsyncManager";
|
||||
|
@ -38,6 +38,7 @@ export function Instance<CK extends ColumnKey>({columns}: InstanceProperties<CK>
|
|||
export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Columns<CK>})
|
||||
{
|
||||
return (
|
||||
<>
|
||||
<tr className={"headings"}>
|
||||
{ // Showing title of each column.
|
||||
Object.keys(columns).map((key) => (
|
||||
|
@ -47,21 +48,44 @@ export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Colum
|
|||
))
|
||||
}
|
||||
</tr>
|
||||
<tr className={"filters"}>
|
||||
{ // Add columns filters, if there are some.
|
||||
(Object.entries(columns) as [CK, Column][]).map(([columnKey, column]) => (
|
||||
column.filter && (
|
||||
<AutoColumnContextProvider key={columnKey as string} columnKey={columnKey}>
|
||||
<td>{column.filter.element}</td>
|
||||
</AutoColumnContextProvider>
|
||||
)
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableBody<CK extends ColumnKey>()
|
||||
{
|
||||
// Get data from table.
|
||||
const {data, columns, columnsSortState} = useTable<CK>();
|
||||
const {data, columns, columnsSortState, columnsFilterStates} = useTable<CK>();
|
||||
|
||||
// Get current data state from the async table value.
|
||||
const {currentDataState} = useAsyncManager<CK>(data);
|
||||
|
||||
// Memorize filtered rows.
|
||||
const filteredRows = useMemo(() => (
|
||||
currentDataState.rows?.filter((row) => (
|
||||
// Checking each row to keep only those which match the filters.
|
||||
(Object.entries(columnsFilterStates) as [CK, any][]).every(([columnKey, filterState]) => (
|
||||
// For each filter, keep the row if data match the current filter.
|
||||
columns[columnKey].filter.filter(row.cells[columnKey].data, filterState)
|
||||
))
|
||||
))
|
||||
), [currentDataState.rows, columnsFilterStates]);
|
||||
|
||||
// Memorize sorted rows.
|
||||
const sortedRows = useMemo(() => (
|
||||
sortRows<CK>(currentDataState.rows, columns, columnsSortState)
|
||||
), [currentDataState.rows, columns, columnsSortState]);
|
||||
sortRows<CK>(filteredRows, columns, columnsSortState)
|
||||
), [filteredRows, columns, columnsSortState]);
|
||||
|
||||
return (
|
||||
sortedRows ? (
|
||||
|
@ -81,7 +105,7 @@ export function TableBody<CK extends ColumnKey>()
|
|||
* @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>[]
|
||||
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;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useContext} from "react";
|
||||
import {ColumnContext, ColumnKey} from "./Column";
|
||||
import {AutoColumnContextProvider, ColumnContext, ColumnKey} from "./Column";
|
||||
import {CellDefinition, CellInstance, CellLoader} from "./Cell";
|
||||
import {Smartable, useTable} from "./Smartable";
|
||||
import { Promisable} from "@kernelui/core";
|
||||
|
@ -91,14 +91,14 @@ export function RowCells()
|
|||
const {columns} = useTable();
|
||||
|
||||
return (
|
||||
Object.entries(columns).map(([columnKey, column]) => (
|
||||
<ColumnContext.Provider key={columnKey} value={{ key: columnKey, column: column }}>
|
||||
{
|
||||
Object.keys(columns).map((columnKey) => (
|
||||
<AutoColumnContextProvider key={columnKey} columnKey={columnKey}>
|
||||
{ // Show current cell.
|
||||
row.cells?.[columnKey]
|
||||
? <CellInstance cell={row.cells?.[columnKey]} />
|
||||
: <CellLoader />
|
||||
}
|
||||
</ColumnContext.Provider>
|
||||
</AutoColumnContextProvider>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -86,10 +86,7 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
|||
return {
|
||||
Table: (props: SmartableProperties<CK>) => {
|
||||
// Initialize sort state.
|
||||
const [sortState, setSortState] = useState({} as Record<CK, SortState>);
|
||||
|
||||
// Filter columns from the given property.
|
||||
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
|
||||
const [sortState, setSortState] = useState({} as Partial<Record<CK, SortState>>);
|
||||
|
||||
// Set sort state of a specific column.
|
||||
const setColumnSortState = useCallback((key: CK, sortType: SortType|null): void => {
|
||||
|
@ -123,13 +120,33 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
|||
setSortState(newSortState);
|
||||
}, [sortState, setSortState]);
|
||||
|
||||
// Initialize filter states.
|
||||
const [filterStates, setFilterStates] = useState<Partial<Record<CK, any>>>({});
|
||||
|
||||
// Set filter state of a specific column.
|
||||
const setColumnFilterState = useCallback((key: CK, filterState: any) => {
|
||||
setFilterStates(
|
||||
{
|
||||
// Copy the other filters states.
|
||||
...filterStates,
|
||||
// Set the filter state for the given column.
|
||||
[key]: filterState,
|
||||
}
|
||||
);
|
||||
}, [filterStates, setFilterStates]);
|
||||
|
||||
// Filter columns from the given property.
|
||||
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
|
||||
|
||||
// Initialize table context value.
|
||||
const contextValue = useMemo<TableContextData<CK>>(() => ({
|
||||
columns: filteredColumns,
|
||||
columnsSortState: sortState,
|
||||
setColumnSortState: setColumnSortState,
|
||||
columnsFilterStates: filterStates,
|
||||
setColumnFilterState: setColumnFilterState,
|
||||
...props,
|
||||
}), [filteredColumns, sortState, setSortState, props]);
|
||||
}), [sortState, setSortState, filterStates, setColumnFilterState, filteredColumns, props]);
|
||||
|
||||
return (
|
||||
<TableContext.Provider value={contextValue}>
|
||||
|
@ -154,14 +171,26 @@ export interface TableContextData<CK extends ColumnKey> extends SmartablePropert
|
|||
/**
|
||||
* Current table columns sort state.
|
||||
*/
|
||||
columnsSortState: Record<CK, SortState>;
|
||||
columnsSortState: Partial<Record<CK, SortState>>;
|
||||
|
||||
/**
|
||||
* Set current table columns sort state.
|
||||
* Set given table column sort state.
|
||||
* @param key The column key for which to set the sort type.
|
||||
* @param sortType The sort type to set for the given column. NULL to reset sort state.
|
||||
*/
|
||||
setColumnSortState: (key: CK, sortType: SortType|null) => void;
|
||||
|
||||
/**
|
||||
* Current table columsn filter states.
|
||||
*/
|
||||
columnsFilterStates: Partial<Record<CK, any>>;
|
||||
|
||||
/**
|
||||
* Set given table column filter state.
|
||||
* @param key The column key for which to set the filter state.
|
||||
* @param filterState The filter state to set for the given column.
|
||||
*/
|
||||
setColumnFilterState: (key: CK, filterState: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
17
src/styles/_filters.less
Normal file
17
src/styles/_filters.less
Normal file
|
@ -0,0 +1,17 @@
|
|||
tr.filters
|
||||
{ // Filters row style.
|
||||
td
|
||||
{
|
||||
padding: 0.25em 0.125em;
|
||||
|
||||
&:first-child:not(:last-child)
|
||||
{
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
input
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
table.smartable
|
||||
{
|
||||
@import "_cells";
|
||||
@import "_filters";
|
||||
@import "_headings";
|
||||
@import "_loaders";
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue