Compare commits
21 commits
trash/asyn
...
main
Author | SHA1 | Date | |
---|---|---|---|
04d28f880f | |||
6b96a85554 | |||
d13f56b24a | |||
79b616dd95 | |||
649d608c89 | |||
cefae670ab | |||
2376d780cc | |||
5e14ebf780 | |||
222242163d | |||
9ef04dbbe1 | |||
0bb88b177f | |||
267afa68dd | |||
71b10a3c89 | |||
519facc608 | |||
f25ca0cc2e | |||
a82da6541d | |||
e71d0aa446 | |||
c8601aaa30 | |||
2753b6eb9f | |||
2513e711b1 | |||
5ac1e4e7b8 |
25 changed files with 1790 additions and 508 deletions
|
@ -1,10 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Application} from "@kernelui/core";
|
import {Application} from "@kernelui/core";
|
||||||
|
import {DemoTable} from "./DemoTable";
|
||||||
|
import {RemoveDemoTable} from "./RemoveDemoTable";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Application>
|
<Application>
|
||||||
|
<h1>Random table</h1>
|
||||||
|
|
||||||
|
<DemoTable />
|
||||||
|
|
||||||
|
<RemoveDemoTable />
|
||||||
</Application>
|
</Application>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
171
demo/DemoTable.tsx
Normal file
171
demo/DemoTable.tsx
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import React, {useMemo} from "react";
|
||||||
|
import {createSmartable, SmartableColumns, SmartableData} from "../src/Smartable/Smartable";
|
||||||
|
import {createColumn, createColumns} from "../src/Smartable/Column";
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
const names: string[] = [
|
||||||
|
"Formica rufa",
|
||||||
|
"Lasius niger",
|
||||||
|
"Camponotus pennsylvanicus",
|
||||||
|
"Solenopsis invicta",
|
||||||
|
"Atta cephalotes",
|
||||||
|
"Pogonomyrmex barbatus",
|
||||||
|
"Myrmica rubra",
|
||||||
|
"Dorymyrmex insanus",
|
||||||
|
"Pheidole megacephala",
|
||||||
|
"Crematogaster scutellaris",
|
||||||
|
"Tetramorium caespitum",
|
||||||
|
"Tapinoma sessile",
|
||||||
|
"Linepithema humile",
|
||||||
|
"Monomorium pharaonis",
|
||||||
|
"Odontomachus bauri",
|
||||||
|
"Paraponera clavata",
|
||||||
|
"Oecophylla smaragdina",
|
||||||
|
"Pseudomyrmex gracilis",
|
||||||
|
"Eciton burchellii",
|
||||||
|
"Anoplolepis gracilipes",
|
||||||
|
"Acromyrmex octospinosus",
|
||||||
|
"Acanthomyops claviger",
|
||||||
|
"Dorylus nigricans",
|
||||||
|
"Neivamyrmex nigrescens",
|
||||||
|
"Hypoponera punctatissima",
|
||||||
|
"Solenopsis geminata",
|
||||||
|
"Camponotus chromaiodes",
|
||||||
|
"Brachymyrmex depilis",
|
||||||
|
"Ectatomma ruidum",
|
||||||
|
"Proceratium silaceum",
|
||||||
|
"Cephalotes atratus",
|
||||||
|
"Neoponera villosa",
|
||||||
|
"Dinoponera gigantea",
|
||||||
|
"Prenolepis imparis",
|
||||||
|
"Lasius flavus",
|
||||||
|
"Formica fusca",
|
||||||
|
"Myrmecia gulosa",
|
||||||
|
"Solenopsis molesta",
|
||||||
|
"Camponotus herculeanus",
|
||||||
|
"Cataulacus granulatus",
|
||||||
|
"Daceton armigerum",
|
||||||
|
"Polyrhachis dives",
|
||||||
|
"Pheidole dentata",
|
||||||
|
"Tetramorium immigrans",
|
||||||
|
"Messor barbarus",
|
||||||
|
"Cardiocondyla obscurior",
|
||||||
|
"Nylanderia flavipes",
|
||||||
|
"Forelius pruinosus",
|
||||||
|
"Amblyopone pallipes"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create main table.
|
||||||
|
const Smartable = createSmartable({
|
||||||
|
columns: createColumns(
|
||||||
|
createColumn("name", {
|
||||||
|
title: "Name",
|
||||||
|
filter: StringFilter,
|
||||||
|
}),
|
||||||
|
createColumn("quantity", {
|
||||||
|
title: "Quantity",
|
||||||
|
filter: NumberFilter,
|
||||||
|
}),
|
||||||
|
createColumn("unit-price", {
|
||||||
|
title: "Unit price",
|
||||||
|
}),
|
||||||
|
createColumn("total-price", {
|
||||||
|
title: "Total",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random quantity.
|
||||||
|
*/
|
||||||
|
export function randomQuantity(): number
|
||||||
|
{
|
||||||
|
return Math.floor(Math.random() * 8) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random unit price.
|
||||||
|
*/
|
||||||
|
export function randomPrice(): number
|
||||||
|
{
|
||||||
|
return Math.floor(Math.random() * 25) + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random name from `names` array.
|
||||||
|
*/
|
||||||
|
export function randomName(): string
|
||||||
|
{
|
||||||
|
return names[Math.floor(Math.random() * names.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random computation time between 0 and 10s.
|
||||||
|
*/
|
||||||
|
export function randomComputationTime(): number
|
||||||
|
{
|
||||||
|
return Math.random() * 1000 * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DemoTable()
|
||||||
|
{
|
||||||
|
const modals = useModals();
|
||||||
|
|
||||||
|
const demoDataPromise = useMemo<SmartableData<SmartableColumns<typeof Smartable>>>(() => (
|
||||||
|
new Promise((resolve) => {
|
||||||
|
// Resolving promise in 2s.
|
||||||
|
window.setTimeout(() => {
|
||||||
|
resolve(Array.from({ length: 43 }).map(() => {
|
||||||
|
// Compute random quantity and unit price.
|
||||||
|
const name = randomName();
|
||||||
|
const quantity = randomQuantity();
|
||||||
|
const price = randomPrice();
|
||||||
|
|
||||||
|
// Fake long computation of total price for each row.
|
||||||
|
const totalPricePromise = new Promise<CellDefinition<number>>((resolve) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
return resolve({
|
||||||
|
data: quantity * price,
|
||||||
|
});
|
||||||
|
}, randomComputationTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cells: {
|
||||||
|
name: {
|
||||||
|
data: name,
|
||||||
|
element: <ClickableCell onClick={() => {
|
||||||
|
const uuid = modals.open(<Modal type={ModalType.INFO} title={name}>
|
||||||
|
A great description about these ants.
|
||||||
|
<Buttons>
|
||||||
|
<button onClick={() => modals.close(uuid)}>OK</button>
|
||||||
|
</Buttons>
|
||||||
|
</Modal>);
|
||||||
|
}} />,
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
data: quantity,
|
||||||
|
},
|
||||||
|
"unit-price": {
|
||||||
|
data: price,
|
||||||
|
},
|
||||||
|
"total-price": totalPricePromise,
|
||||||
|
},
|
||||||
|
} as RowDefinition<SmartableColumns<typeof Smartable>>;
|
||||||
|
}));
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Smartable.Table data={demoDataPromise} paginate={{ pageSize: 6 }} />
|
||||||
|
);
|
||||||
|
}
|
95
demo/RemoveDemoTable.tsx
Normal file
95
demo/RemoveDemoTable.tsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {createSmartable} from "../src/Smartable/Smartable";
|
||||||
|
import {createColumn, createColumns} from "../src/Smartable/Column";
|
||||||
|
import {StringFilter} from "../src/Smartable/Filters/StringFilter";
|
||||||
|
import { Cell } from "../src/Smartable/Cell";
|
||||||
|
|
||||||
|
|
||||||
|
// Create main table.
|
||||||
|
const Smartable = createSmartable({
|
||||||
|
columns: createColumns(
|
||||||
|
createColumn("name", {
|
||||||
|
title: "Name",
|
||||||
|
filter: StringFilter,
|
||||||
|
}),
|
||||||
|
createColumn("actions", {
|
||||||
|
title: "Actions",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function RemoveDemoTable()
|
||||||
|
{
|
||||||
|
const [rows, setRows] = useState<string[]>([
|
||||||
|
"Formica rufa",
|
||||||
|
"Lasius niger",
|
||||||
|
"Camponotus pennsylvanicus",
|
||||||
|
"Solenopsis invicta",
|
||||||
|
"Atta cephalotes",
|
||||||
|
"Pogonomyrmex barbatus",
|
||||||
|
"Myrmica rubra",
|
||||||
|
"Dorymyrmex insanus",
|
||||||
|
"Pheidole megacephala",
|
||||||
|
"Crematogaster scutellaris",
|
||||||
|
"Tetramorium caespitum",
|
||||||
|
"Tapinoma sessile",
|
||||||
|
"Linepithema humile",
|
||||||
|
"Monomorium pharaonis",
|
||||||
|
"Odontomachus bauri",
|
||||||
|
"Paraponera clavata",
|
||||||
|
"Oecophylla smaragdina",
|
||||||
|
"Pseudomyrmex gracilis",
|
||||||
|
"Eciton burchellii",
|
||||||
|
"Anoplolepis gracilipes",
|
||||||
|
"Acromyrmex octospinosus",
|
||||||
|
"Acanthomyops claviger",
|
||||||
|
"Dorylus nigricans",
|
||||||
|
"Neivamyrmex nigrescens",
|
||||||
|
"Hypoponera punctatissima",
|
||||||
|
"Solenopsis geminata",
|
||||||
|
"Camponotus chromaiodes",
|
||||||
|
"Brachymyrmex depilis",
|
||||||
|
"Ectatomma ruidum",
|
||||||
|
"Proceratium silaceum",
|
||||||
|
"Cephalotes atratus",
|
||||||
|
"Neoponera villosa",
|
||||||
|
"Dinoponera gigantea",
|
||||||
|
"Prenolepis imparis",
|
||||||
|
"Lasius flavus",
|
||||||
|
"Formica fusca",
|
||||||
|
"Myrmecia gulosa",
|
||||||
|
"Solenopsis molesta",
|
||||||
|
"Camponotus herculeanus",
|
||||||
|
"Cataulacus granulatus",
|
||||||
|
"Daceton armigerum",
|
||||||
|
"Polyrhachis dives",
|
||||||
|
"Pheidole dentata",
|
||||||
|
"Tetramorium immigrans",
|
||||||
|
"Messor barbarus",
|
||||||
|
"Cardiocondyla obscurior",
|
||||||
|
"Nylanderia flavipes",
|
||||||
|
"Forelius pruinosus",
|
||||||
|
"Amblyopone pallipes",
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Smartable.Table data={rows.map((row, rowIndex) => ({
|
||||||
|
cells: {
|
||||||
|
name: {
|
||||||
|
data: row,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
data: null,
|
||||||
|
element: (
|
||||||
|
<Cell>
|
||||||
|
<button className={"remove"}
|
||||||
|
onClick={() => setRows(rows.toSpliced(rowIndex, 1))}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))} disableFilter={true} disableSort={true} />
|
||||||
|
);
|
||||||
|
}
|
16
index.ts
16
index.ts
|
@ -1,2 +1,18 @@
|
||||||
import "./src/styles/smartable.less";
|
import "./src/styles/smartable.less";
|
||||||
|
|
||||||
|
export * from "./src/Smartable/AsyncManager";
|
||||||
|
export * from "./src/Smartable/Cell";
|
||||||
|
export * from "./src/Smartable/Column";
|
||||||
|
export * from "./src/Smartable/Instance";
|
||||||
|
export * from "./src/Smartable/Row";
|
||||||
|
export * from "./src/Smartable/Smartable";
|
||||||
|
export * from "./src/Smartable/Sort";
|
||||||
|
|
||||||
|
export * from "./src/Smartable/Cells/ClickableCell";
|
||||||
|
|
||||||
|
export * from "./src/Smartable/Columns/ColumnFilter";
|
||||||
|
export * from "./src/Smartable/Columns/ColumnHeading";
|
||||||
|
|
||||||
|
export * from "./src/Smartable/Filters/EnumFilter";
|
||||||
|
export * from "./src/Smartable/Filters/NumberFilter";
|
||||||
|
export * from "./src/Smartable/Filters/StringFilter";
|
||||||
|
|
16
package.json
16
package.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.0-rc1",
|
"version": "1.1.1",
|
||||||
"name": "@kernelui/smartable",
|
"name": "@kernelui/smartable",
|
||||||
"description": "Kernel UI Smartable.",
|
"description": "Kernel UI Smartable.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -16,17 +16,23 @@
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
|
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"@kernelui/core": "^1.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kernelui/core": "^1.6.0",
|
||||||
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-dts": "^3.9.1"
|
"vite-plugin-dts": "^3.9.1"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.2.2"
|
"peerDependencies": {
|
||||||
|
"@kernelui/core": "^1.1.2"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
|
355
src/Smartable/AsyncManager.tsx
Normal file
355
src/Smartable/AsyncManager.tsx
Normal file
|
@ -0,0 +1,355 @@
|
||||||
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
|
import {ColumnKey} from "./Column";
|
||||||
|
import {normalizeRowDefinition, RowData, RowDefinition} from "./Row";
|
||||||
|
import {CellDefinition} from "./Cell";
|
||||||
|
import {SmartableData} from "./Smartable";
|
||||||
|
import {Modify, Promisable} from "@kernelui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current Smartable data state.
|
||||||
|
*/
|
||||||
|
export interface CurrentTableData<CK extends ColumnKey>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Current rows state, undefined if they are still loading.
|
||||||
|
*/
|
||||||
|
rows?: CurrentRowData<CK>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable current row cells data state.
|
||||||
|
*/
|
||||||
|
export type CurrentRowData<CK extends ColumnKey, T = any> = Modify<RowData<CK>, {
|
||||||
|
cells: Record<CK, CellDefinition<T>>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current async table data manager.
|
||||||
|
*/
|
||||||
|
export function useAsyncManager<CK extends ColumnKey>(data: SmartableData<CK>): AsyncManager<CK>
|
||||||
|
{
|
||||||
|
// Get the main instance of async manager.
|
||||||
|
const asyncManager = useRef<AsyncManager<CK>>();
|
||||||
|
|
||||||
|
// Get the main async manager state.
|
||||||
|
const [currentDataState, setCurrentDataState] = useState<CurrentTableData<CK>>({
|
||||||
|
rows: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!asyncManager.current)
|
||||||
|
{
|
||||||
|
// Initialize a new async manager if there is none.
|
||||||
|
asyncManager.current = new AsyncManager<CK>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current data state and its dispatcher.
|
||||||
|
asyncManager.current.currentDataState = currentDataState;
|
||||||
|
asyncManager.current.setCurrentDataState = setCurrentDataState;
|
||||||
|
|
||||||
|
// When defined table data change, process async state again.
|
||||||
|
useEffect(() => {
|
||||||
|
// Process new data.
|
||||||
|
asyncManager.current.handle(data);
|
||||||
|
}, [asyncManager, data]);
|
||||||
|
|
||||||
|
// Return the main instance of async manager.
|
||||||
|
return asyncManager.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable async data manager.
|
||||||
|
*/
|
||||||
|
class AsyncManager<CK extends ColumnKey>
|
||||||
|
{
|
||||||
|
currentDataState: CurrentTableData<CK>;
|
||||||
|
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main promised data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected promisedData: Promised<SmartableData<CK>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promised rows data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected promisedRows: Promised<RowDefinition<CK>>[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promised rows cells data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected promisedRowsCells: Partial<Record<CK, Promised<CellDefinition>>>[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if rows are loaded or not.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected rowsLoaded: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if rows need to be reinitialized or not.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected reinitRows: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rows data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected rowsData: RowData<CK>[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rows cells definitions.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected cellsDefinitions: Partial<Record<CK, CellDefinition<CK>>>[] = [];
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
// Initialize promised data object.
|
||||||
|
this.promisedData = new Promised(this.handleNewRowsDefinitions.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new smartable data.
|
||||||
|
* @param data Smartable data.
|
||||||
|
*/
|
||||||
|
handle(data: SmartableData<CK>): void
|
||||||
|
{
|
||||||
|
this.rowsLoaded = false;
|
||||||
|
this.promisedData.refresh(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle new rows definitions.
|
||||||
|
* @param rowsDefinitions Rows definitions.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewRowsDefinitions(rowsDefinitions: Promisable<RowDefinition<CK>>[]): void
|
||||||
|
{
|
||||||
|
// Ignore undefined value.
|
||||||
|
if (rowsDefinitions == undefined) return;
|
||||||
|
|
||||||
|
// Rows have been reinitialized.
|
||||||
|
this.reinitRows = true;
|
||||||
|
|
||||||
|
// Initialize rows data and cells definitions.
|
||||||
|
this.rowsData = [];
|
||||||
|
this.cellsDefinitions = [];
|
||||||
|
this.rowsLoaded = true;
|
||||||
|
|
||||||
|
for (const [rowId, rowDefinition] of rowsDefinitions.entries())
|
||||||
|
{ // Get row data of each row.
|
||||||
|
if (!this.promisedRows[rowId])
|
||||||
|
this.promisedRows[rowId] = new Promised(this.handleNewRow.bind(this, rowId), rowDefinition);
|
||||||
|
else
|
||||||
|
this.promisedRows[rowId].refresh(rowDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to update the current data state.
|
||||||
|
this.tryToUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a new row.
|
||||||
|
* @param rowId Row ID.
|
||||||
|
* @param row Row definition.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewRow(rowId: number, row: RowDefinition<CK>): void
|
||||||
|
{
|
||||||
|
// Ignore undefined value.
|
||||||
|
if (row == undefined) return;
|
||||||
|
|
||||||
|
// Normalize row data.
|
||||||
|
const rowData = normalizeRowDefinition(row);
|
||||||
|
|
||||||
|
// Save row data.
|
||||||
|
this.rowsData[rowId] = rowData;
|
||||||
|
// Initialize cells definition.
|
||||||
|
this.cellsDefinitions[rowId] = {};
|
||||||
|
|
||||||
|
for (const [columnKey, cellDefinition] of Object.entries(rowData.cells) as [CK, CellDefinition][])
|
||||||
|
{ // Get cell definition of each row cell.
|
||||||
|
if (!this.promisedRowsCells[rowId]) this.promisedRowsCells[rowId] = {};
|
||||||
|
if (!this.promisedRowsCells[rowId][columnKey])
|
||||||
|
this.promisedRowsCells[rowId][columnKey] = new Promised(this.handleNewCell.bind(this, rowId, columnKey), cellDefinition);
|
||||||
|
else
|
||||||
|
this.promisedRowsCells[rowId][columnKey].refresh(cellDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to update the current data state.
|
||||||
|
this.tryToUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a new cell.
|
||||||
|
* @param rowId Cell row ID.
|
||||||
|
* @param columnKey Cell column key.
|
||||||
|
* @param cellDefinition Cell definition.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewCell(rowId: number, columnKey: CK, cellDefinition: CellDefinition): void
|
||||||
|
{
|
||||||
|
// Ignore undefined value.
|
||||||
|
if (cellDefinition == undefined) return;
|
||||||
|
|
||||||
|
// Save cell definition.
|
||||||
|
this.cellsDefinitions[rowId][columnKey] = cellDefinition;
|
||||||
|
|
||||||
|
// Try to update the current data state.
|
||||||
|
this.tryToUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if there is a pending update.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected pendingUpdate: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to update the current data state, if there are no more changes in the next 25ms.
|
||||||
|
* The next update will be in max 25ms.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected tryToUpdate(): void
|
||||||
|
{
|
||||||
|
if (!this.pendingUpdate)
|
||||||
|
// Try to update in the next 25ms.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.pendingUpdate = false;
|
||||||
|
this.update();
|
||||||
|
}, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current data state with the loaded rows and cells data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected update(): void
|
||||||
|
{
|
||||||
|
if (!(
|
||||||
|
// Checking that there is at least one changed value.
|
||||||
|
this.reinitRows ||
|
||||||
|
this.rowsData.length > 0 || this.cellsDefinitions.some((rowCells) => (
|
||||||
|
Object.keys(rowCells).length > 0
|
||||||
|
))
|
||||||
|
|| ((this.rowsLoaded && !this.currentDataState.rows) || (!this.rowsLoaded && this.currentDataState.rows))
|
||||||
|
)) // Nothing has changed.
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Initialize new data.
|
||||||
|
const newData = {
|
||||||
|
rows: !this.rowsLoaded ? undefined : this.reinitRows ? [] : [
|
||||||
|
...(this.currentDataState?.rows ?? [])
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rows have been reinitialized.
|
||||||
|
this.reinitRows = false;
|
||||||
|
|
||||||
|
for (const [rowId, newRow] of this.rowsData?.entries())
|
||||||
|
{ // Update value of each new row.
|
||||||
|
newData.rows[rowId] = {
|
||||||
|
element: newRow.element,
|
||||||
|
cellElement: newRow.cellElement,
|
||||||
|
cells: newData.rows[rowId]?.cells,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [rowId, rowCells] of this.cellsDefinitions?.entries())
|
||||||
|
{ // Update cells of each changed row.
|
||||||
|
newData.rows[rowId] = {
|
||||||
|
...newData.rows[rowId],
|
||||||
|
cells: {...(newData.rows[rowId]?.cells ?? {}), ...rowCells} as Record<CK, CellDefinition>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the current data state.
|
||||||
|
this.currentDataState = newData;
|
||||||
|
this.setCurrentDataState(
|
||||||
|
newData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promised data class.
|
||||||
|
*/
|
||||||
|
export class Promised<T>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The main data promise.
|
||||||
|
*/
|
||||||
|
promise?: Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data retrieved from promise or given in parameter.
|
||||||
|
*/
|
||||||
|
data?: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when data is changed.
|
||||||
|
*/
|
||||||
|
onData?: (data: T) => void;
|
||||||
|
|
||||||
|
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
|
||||||
|
{
|
||||||
|
this.onData = onChanged;
|
||||||
|
if (data)
|
||||||
|
this.refresh(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the promised data.
|
||||||
|
* @param data Promised data.
|
||||||
|
*/
|
||||||
|
refresh(data: Promisable<T>): this
|
||||||
|
{
|
||||||
|
if (data instanceof Promise)
|
||||||
|
{ // We have a promise of data.
|
||||||
|
if (data != this.promise)
|
||||||
|
{ // The promise is different from the saved one.
|
||||||
|
// Save the new promise and set data to undefined.
|
||||||
|
this.promise = data;
|
||||||
|
this.data = undefined;
|
||||||
|
this.onData?.(undefined);
|
||||||
|
|
||||||
|
// Wait for promise to resolve to get actual data.
|
||||||
|
this.promise.then((data) => {
|
||||||
|
// Data is retrieved, saving it.
|
||||||
|
this.data = data;
|
||||||
|
this.onData?.(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data != this.data)
|
||||||
|
{ // We already have data, and it is different from the current state.
|
||||||
|
this.data = data;
|
||||||
|
this.onData?.(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if some data (or its promise) have been provided.
|
||||||
|
*/
|
||||||
|
isInitialized(): boolean
|
||||||
|
{
|
||||||
|
return !!this.data || !!this.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if we are waiting for a promise result.
|
||||||
|
*/
|
||||||
|
isLoading(): boolean
|
||||||
|
{
|
||||||
|
return this.data === undefined && !!this.promise;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,14 +22,14 @@ export interface CellDefinition<T = any>
|
||||||
/**
|
/**
|
||||||
* Default cell component.
|
* Default cell component.
|
||||||
*/
|
*/
|
||||||
export function Cell({children}: React.PropsWithChildren<{}>)
|
export function Cell({children, ...props}: React.PropsWithChildren<React.TdHTMLAttributes<HTMLTableCellElement>>)
|
||||||
{
|
{
|
||||||
// Get cell data.
|
// Get cell data.
|
||||||
const {data} = useCell();
|
const {data} = useCell();
|
||||||
|
|
||||||
// Try to render cell data to string when no children given.
|
// Try to render cell data to string when no children given.
|
||||||
return (
|
return (
|
||||||
<td>{children ?? String(data)}</td>
|
<td {...props}>{children ?? String(data)}</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,3 +67,11 @@ export function CellInstance<T>({cell}: {cell: CellDefinition<T>})
|
||||||
</CellContext.Provider>
|
</CellContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated cell loader.
|
||||||
|
*/
|
||||||
|
export function CellLoader()
|
||||||
|
{
|
||||||
|
return <td className={"generic loader"}></td>;
|
||||||
|
}
|
||||||
|
|
17
src/Smartable/Cells/ClickableCell.tsx
Normal file
17
src/Smartable/Cells/ClickableCell.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import {Cell} from "../Cell";
|
||||||
|
import {Modify} from "@kernelui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clickable cell component.
|
||||||
|
*/
|
||||||
|
export function ClickableCell({role, children, ...props}: React.PropsWithChildren<Modify<React.TdHTMLAttributes<HTMLTableCellElement>, {
|
||||||
|
role?: never;
|
||||||
|
}>>)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Cell role={"button"} {...props}>
|
||||||
|
{children}
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +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 {SortState} from "./Sort";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic column key type.
|
* Basic column key type.
|
||||||
|
@ -25,7 +27,7 @@ export function createColumns<K extends ColumnKey>(...columns: [K, Column][]): C
|
||||||
/**
|
/**
|
||||||
* Smartable column definition.
|
* Smartable column definition.
|
||||||
*/
|
*/
|
||||||
export interface Column
|
export interface Column<T = any>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Column title element.
|
* Column title element.
|
||||||
|
@ -36,6 +38,18 @@ export interface Column
|
||||||
* Column cell default element.
|
* Column cell default element.
|
||||||
*/
|
*/
|
||||||
cellElement?: React.ReactElement;
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column filter definition.
|
||||||
|
*/
|
||||||
|
filter?: ColumnFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,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.
|
||||||
*/
|
*/
|
||||||
|
@ -92,6 +81,17 @@ export interface ColumnContextData<CK extends ColumnKey>
|
||||||
* Column sort state.
|
* Column sort state.
|
||||||
*/
|
*/
|
||||||
sortState?: SortState;
|
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);
|
export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined);
|
||||||
|
@ -104,45 +104,6 @@ export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): Colu
|
||||||
return useContext(ColumnContext);
|
return useContext(ColumnContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto column context provider from table context.
|
* Auto column context provider from table context.
|
||||||
*/
|
*/
|
||||||
|
@ -151,6 +112,12 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
||||||
// Get table data.
|
// Get table data.
|
||||||
const table = useTable();
|
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 (
|
return (
|
||||||
<ColumnContext.Provider value={{
|
<ColumnContext.Provider value={{
|
||||||
key: columnKey,
|
key: columnKey,
|
||||||
|
@ -158,6 +125,10 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
||||||
column: table.columns[columnKey],
|
column: table.columns[columnKey],
|
||||||
// Get current column sort state from table data.
|
// Get current column sort state from table data.
|
||||||
sortState: table.columnsSortState?.[columnKey],
|
sortState: table.columnsSortState?.[columnKey],
|
||||||
|
// Get current column filter state from table data.
|
||||||
|
filterState: table.columnsFilterStates?.[columnKey],
|
||||||
|
// Current column filter state dispatcher.
|
||||||
|
setFilterState: setFilterState,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</ColumnContext.Provider>
|
</ColumnContext.Provider>
|
||||||
|
|
32
src/Smartable/Columns/ColumnFilter.tsx
Normal file
32
src/Smartable/Columns/ColumnFilter.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React from "react";
|
||||||
|
import {useColumn} from "../Column";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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];
|
||||||
|
}
|
45
src/Smartable/Columns/ColumnHeading.tsx
Normal file
45
src/Smartable/Columns/ColumnHeading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
69
src/Smartable/Filters/EnumFilter.tsx
Normal file
69
src/Smartable/Filters/EnumFilter.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React, {useCallback} from "react";
|
||||||
|
import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter";
|
||||||
|
import {Select} from "@kernelui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String column filter.
|
||||||
|
*/
|
||||||
|
export function EnumFilter<OptionKey extends keyof any, Option>(options: Record<OptionKey, Option>): ColumnFilter<OptionKey[], EnumFilterState<OptionKey>>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
filter: (data: OptionKey|OptionKey[], filterState: EnumFilterState<OptionKey>) => {
|
||||||
|
// Normalized data array.
|
||||||
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
|
|
||||||
|
// Nothing in filter value, allow everything.
|
||||||
|
if (!filterState?.value?.length) return true;
|
||||||
|
|
||||||
|
// Get current filter result.
|
||||||
|
return filterState?.value?.some(
|
||||||
|
// Keep the row if any element in data match the current filter value.
|
||||||
|
(optionKey) => dataArray.includes(optionKey)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
element: <EnumFilterComponent options={options} />
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration filter state.
|
||||||
|
*/
|
||||||
|
export interface EnumFilterState<OptionKey extends keyof any>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Filter value.
|
||||||
|
*/
|
||||||
|
value: OptionKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum filter component.
|
||||||
|
*/
|
||||||
|
export function EnumFilterComponent<OptionKey extends keyof any, Option>({options}: {
|
||||||
|
/**
|
||||||
|
* Enum options.
|
||||||
|
*/
|
||||||
|
options: Record<OptionKey, Option>;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// Initialize enum filter state.
|
||||||
|
const [enumFilterState, setEnumFilterState] =
|
||||||
|
useFilterState<EnumFilterState<OptionKey>>({ value: [] });
|
||||||
|
|
||||||
|
// Handle filter input change.
|
||||||
|
const handleChange = useCallback((filter: OptionKey[]) => {
|
||||||
|
// Save the current filter value.
|
||||||
|
setEnumFilterState({
|
||||||
|
value: filter,
|
||||||
|
})
|
||||||
|
}, [setEnumFilterState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select<OptionKey, Option> options={options} multiple={true} size={1}
|
||||||
|
className={!enumFilterState.value?.length ? "empty" : undefined}
|
||||||
|
value={enumFilterState.value} onChange={handleChange} />
|
||||||
|
)
|
||||||
|
}
|
71
src/Smartable/Filters/NumberFilter.tsx
Normal file
71
src/Smartable/Filters/NumberFilter.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React, {useCallback} from "react";
|
||||||
|
import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter value regex.
|
||||||
|
*/
|
||||||
|
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} />
|
||||||
|
);
|
||||||
|
}
|
60
src/Smartable/Filters/StringFilter.tsx
Normal file
60
src/Smartable/Filters/StringFilter.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React, {useCallback} from "react";
|
||||||
|
import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter";
|
||||||
|
import {normalizeString} from "@kernelui/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter value regex.
|
||||||
|
*/
|
||||||
|
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,8 +1,11 @@
|
||||||
import React from "react";
|
import React, {useMemo} from "react";
|
||||||
import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column";
|
import {AutoColumnContextProvider, Column, ColumnKey, Columns} from "./Column";
|
||||||
import {SmartableProperties, useTable} from "./Smartable";
|
import {SmartableProperties, useTable} from "./Smartable";
|
||||||
import {Async, Promisable} from "@kernelui/core";
|
import {RowInstance, RowLoader} from "./Row";
|
||||||
import {RowDefinition, RowInstance} from "./Row";
|
import {useAsyncManager} from "./AsyncManager";
|
||||||
|
import {ColumnHeading} from "./Columns/ColumnHeading";
|
||||||
|
import {sortRows} from "./Sort";
|
||||||
|
import {AutoPaginate, classes} from "@kernelui/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smartable instance component properties.
|
* Smartable instance component properties.
|
||||||
|
@ -18,15 +21,60 @@ export interface InstanceProperties<CK extends ColumnKey> extends SmartablePrope
|
||||||
/**
|
/**
|
||||||
* Main component for a Smartable table.
|
* Main component for a Smartable table.
|
||||||
*/
|
*/
|
||||||
export function Instance<CK extends ColumnKey>({columns}: InstanceProperties<CK>)
|
export function Instance<CK extends ColumnKey>(props: InstanceProperties<CK>)
|
||||||
|
{
|
||||||
|
if (props.paginate)
|
||||||
|
{ // If pagination is enabled.
|
||||||
|
return <PaginatedInstance {...props} />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // No pagination, simple body render.
|
||||||
|
return (
|
||||||
|
<Table {...props}>
|
||||||
|
<TableBody />
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated Smartable instance component.
|
||||||
|
*/
|
||||||
|
export function PaginatedInstance<CK extends ColumnKey>(props: InstanceProperties<CK>)
|
||||||
|
{
|
||||||
|
// Get data from table.
|
||||||
|
const {data} = useTable<CK>();
|
||||||
|
|
||||||
|
// Get current data state from the async table value.
|
||||||
|
const {currentDataState} = useAsyncManager<CK>(data);
|
||||||
|
|
||||||
|
// Compute page count for the current rows.
|
||||||
|
const pageCount = Math.ceil((currentDataState?.rows?.length ?? 0) / props.paginate.pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoPaginate count={pageCount}>
|
||||||
|
{(page) => (
|
||||||
|
// Render table with table body of the current page.
|
||||||
|
<Table {...props}>
|
||||||
|
<TableBody pagination={{page: page, pageSize: props.paginate.pageSize}} />
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</AutoPaginate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base component for a Smartable table.
|
||||||
|
*/
|
||||||
|
export function Table<CK extends ColumnKey>({className, columns, children}: React.PropsWithChildren<InstanceProperties<CK>>)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<table>
|
<table className={classes("smartable", className)}>
|
||||||
<thead>
|
<thead>
|
||||||
<ColumnsHeadings columns={columns} />
|
<ColumnsHeadings columns={columns} />
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<TableBody />
|
{children}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
@ -37,8 +85,12 @@ export function Instance<CK extends ColumnKey>({columns}: InstanceProperties<CK>
|
||||||
*/
|
*/
|
||||||
export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Columns<CK>})
|
export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Columns<CK>})
|
||||||
{
|
{
|
||||||
|
// Get feature disable options.
|
||||||
|
const {disableSort, disableFilter} = useTable<CK>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={"headings"}>
|
<>
|
||||||
|
<tr className={classes("headings", disableSort === true ? "disable-sort" : undefined)}>
|
||||||
{ // Showing title of each column.
|
{ // Showing title of each column.
|
||||||
Object.keys(columns).map((key) => (
|
Object.keys(columns).map((key) => (
|
||||||
<AutoColumnContextProvider key={key} columnKey={key}>
|
<AutoColumnContextProvider key={key} columnKey={key}>
|
||||||
|
@ -47,31 +99,72 @@ export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Colum
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
{ // Add filters if filter feature is not disabled.
|
||||||
}
|
disableFilter !== true && (
|
||||||
|
<tr className={"filters"}>
|
||||||
export function TableBody<CK extends ColumnKey>()
|
{ // Add columns filters, if there are some.
|
||||||
{
|
(Object.entries(columns) as [CK, Column][]).map(([columnKey, column]) => (
|
||||||
// Get data from table.
|
column.filter && (
|
||||||
const {data} = useTable();
|
<AutoColumnContextProvider key={columnKey as string} columnKey={columnKey}>
|
||||||
|
<td>{column.filter.element}</td>
|
||||||
return (
|
</AutoColumnContextProvider>
|
||||||
<Async<(Promisable<RowDefinition<CK>>)[]> promise={data}>
|
)
|
||||||
{(rowsData) => (
|
|
||||||
// Rendering defined rows.
|
|
||||||
<>
|
|
||||||
{ // Rendering each row.
|
|
||||||
rowsData.map((rowData, index) => (
|
|
||||||
// Rendering current row from its definition.
|
|
||||||
<Async<RowDefinition<CK>> key={index} promise={rowData}>
|
|
||||||
{(rowDefinition) => (
|
|
||||||
<RowInstance row={rowDefinition} />
|
|
||||||
)}
|
|
||||||
</Async>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
</Async>
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable table body.
|
||||||
|
*/
|
||||||
|
export function TableBody<CK extends ColumnKey>({pagination}: {
|
||||||
|
/**
|
||||||
|
* Current pagination state.
|
||||||
|
*/
|
||||||
|
pagination?: { page: number; pageSize: number; };
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// Get data from table.
|
||||||
|
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.
|
||||||
|
let sortedRows = useMemo(() => (
|
||||||
|
// Sort rows with the current columns sort state.
|
||||||
|
sortRows<CK>(filteredRows, columns, columnsSortState)
|
||||||
|
), [filteredRows, columns, columnsSortState]);
|
||||||
|
|
||||||
|
if (pagination)
|
||||||
|
{ // If pagination is enabled, showing only content of the current page.
|
||||||
|
const startIndex = (pagination.page - 1) * pagination.pageSize;
|
||||||
|
sortedRows = sortedRows?.slice(startIndex, startIndex + pagination.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
sortedRows ? (
|
||||||
|
sortedRows.map((rowData, index) => (
|
||||||
|
// Rendering each row from its definition.
|
||||||
|
<RowInstance key={index} row={rowData} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<RowLoader />
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, {useContext, useMemo} from "react";
|
import React, {useContext} from "react";
|
||||||
import {ColumnContext, ColumnKey} from "./Column";
|
import {AutoColumnContextProvider, ColumnContext, ColumnKey} from "./Column";
|
||||||
import {CellDefinition, CellInstance} from "./Cell";
|
import {CellDefinition, CellInstance, CellLoader} from "./Cell";
|
||||||
import {Smartable, useTable} from "./Smartable";
|
import {Smartable, useTable} from "./Smartable";
|
||||||
import {Async, Promisable} from "@kernelui/core";
|
import { Promisable} from "@kernelui/core";
|
||||||
|
import {CurrentRowData} from "./AsyncManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smartable row cells.
|
* Smartable row cells.
|
||||||
|
@ -35,10 +36,27 @@ export interface RowData<CK extends ColumnKey, T = any>
|
||||||
*/
|
*/
|
||||||
export type RowDefinition<CK extends ColumnKey> = RowCells<CK>|RowData<CK>;
|
export type RowDefinition<CK extends ColumnKey> = RowCells<CK>|RowData<CK>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize row definition to row data.
|
||||||
|
*/
|
||||||
|
export function normalizeRowDefinition<CK extends ColumnKey>(rowDefinition: RowDefinition<CK>): RowData<CK>
|
||||||
|
{
|
||||||
|
if (!("cells" in rowDefinition) || !Object.values(rowDefinition?.cells).some((cellData: Promisable<CellDefinition>) => (
|
||||||
|
cellData instanceof Promise || ("data" in cellData)
|
||||||
|
))) { // If the row definition doesn't form a RowData object (= it is a RowCell object), converting it.
|
||||||
|
rowDefinition = {
|
||||||
|
cells: rowDefinition as RowCells<CK>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return changed row definition, or just keep the default one if it matched RowData.
|
||||||
|
return rowDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table row context data.
|
* Table row context data.
|
||||||
*/
|
*/
|
||||||
export interface RowContextData<CK extends ColumnKey, T = any> extends RowData<CK, T>
|
export interface RowContextData<CK extends ColumnKey, T = any> extends CurrentRowData<CK, T>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,14 +91,14 @@ export function RowCells()
|
||||||
const {columns} = useTable();
|
const {columns} = useTable();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Object.entries(columns).map(([columnKey, column]) => (
|
Object.keys(columns).map((columnKey) => (
|
||||||
<ColumnContext.Provider key={columnKey} value={{ key: columnKey, column: column }}>
|
<AutoColumnContextProvider key={columnKey} columnKey={columnKey}>
|
||||||
<Async<CellDefinition> promise={row.cells?.[columnKey] ?? { data: undefined }}>
|
{ // Show current cell.
|
||||||
{(cellDefinition) => (
|
row.cells?.[columnKey]
|
||||||
<CellInstance cell={cellDefinition} />
|
? <CellInstance cell={row.cells?.[columnKey]} />
|
||||||
)}
|
: <CellLoader />
|
||||||
</Async>
|
}
|
||||||
</ColumnContext.Provider>
|
</AutoColumnContextProvider>
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,21 +106,29 @@ export function RowCells()
|
||||||
/**
|
/**
|
||||||
* Row instance component.
|
* Row instance component.
|
||||||
*/
|
*/
|
||||||
export function RowInstance<CK extends ColumnKey>({row}: { row: RowDefinition<CK> })
|
export function RowInstance<CK extends ColumnKey>({row}: { row: CurrentRowData<CK> })
|
||||||
{
|
{
|
||||||
// Get row context value from given row definition.
|
// Get table row element.
|
||||||
const rowContextValue = useMemo(() => (
|
|
||||||
// If a simple RowCells<CK> object is given, converting it to a RowData<CK>
|
|
||||||
!("cells" in row) ? { cells: row } : row
|
|
||||||
), [row]);
|
|
||||||
|
|
||||||
const {rowElement} = useTable();
|
const {rowElement} = useTable();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowContext.Provider value={rowContextValue}>
|
<RowContext.Provider value={row}>
|
||||||
{ // Trying to render row-specific element, then table-specific element, then default element.
|
{ // Trying to render row-specific element, then table-specific element, then default element.
|
||||||
rowContextValue.element ?? rowElement ?? <Row />
|
row.element ?? rowElement ?? <Row />
|
||||||
}
|
}
|
||||||
</RowContext.Provider>
|
</RowContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animated row loader.
|
||||||
|
*/
|
||||||
|
export function RowLoader()
|
||||||
|
{
|
||||||
|
// Get table columns to get their count.
|
||||||
|
const {columns} = useTable();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr><td colSpan={Object.keys(columns).length} className={"generic loader"}></td></tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -14,6 +15,11 @@ export type SmartableData<CK extends ColumnKey> = Promisable<(Promisable<RowDefi
|
||||||
*/
|
*/
|
||||||
export interface SmartableProperties<CK extends ColumnKey>
|
export interface SmartableProperties<CK extends ColumnKey>
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Table custom class name.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table data.
|
* Table data.
|
||||||
*/
|
*/
|
||||||
|
@ -33,6 +39,26 @@ export interface SmartableProperties<CK extends ColumnKey>
|
||||||
* Default column heading element.
|
* Default column heading element.
|
||||||
*/
|
*/
|
||||||
columnHeadingElement?: React.ReactElement;
|
columnHeadingElement?: React.ReactElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table rows pagination.
|
||||||
|
*/
|
||||||
|
paginate?: {
|
||||||
|
/**
|
||||||
|
* Number of rows per page.
|
||||||
|
*/
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable sort feature.
|
||||||
|
*/
|
||||||
|
disableSort?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable filter feature.
|
||||||
|
*/
|
||||||
|
disableFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,6 +71,11 @@ export type Smartable<CK extends ColumnKey> = {
|
||||||
Table: React.FunctionComponent<SmartableProperties<CK>>;
|
Table: React.FunctionComponent<SmartableProperties<CK>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smartable columns infered type.
|
||||||
|
*/
|
||||||
|
export type SmartableColumns<CK> = CK extends Smartable<infer CK> ? CK : never;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a new Smartable.
|
* Define a new Smartable.
|
||||||
*/
|
*/
|
||||||
|
@ -81,10 +112,7 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
||||||
return {
|
return {
|
||||||
Table: (props: SmartableProperties<CK>) => {
|
Table: (props: SmartableProperties<CK>) => {
|
||||||
// Initialize sort state.
|
// Initialize sort state.
|
||||||
const [sortState, setSortState] = useState({} as Record<CK, SortState>);
|
const [sortState, setSortState] = useState({} as Partial<Record<CK, SortState>>);
|
||||||
|
|
||||||
// Filter columns from the given property.
|
|
||||||
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
|
|
||||||
|
|
||||||
// Set sort state of a specific column.
|
// Set sort state of a specific column.
|
||||||
const setColumnSortState = useCallback((key: CK, sortType: SortType|null): void => {
|
const setColumnSortState = useCallback((key: CK, sortType: SortType|null): void => {
|
||||||
|
@ -118,13 +146,33 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
||||||
setSortState(newSortState);
|
setSortState(newSortState);
|
||||||
}, [sortState, setSortState]);
|
}, [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.
|
// Initialize table context value.
|
||||||
const contextValue = useMemo<TableContextData<CK>>(() => ({
|
const contextValue = useMemo<TableContextData<CK>>(() => ({
|
||||||
columns: filteredColumns,
|
columns: filteredColumns,
|
||||||
columnsSortState: sortState,
|
columnsSortState: sortState,
|
||||||
setColumnSortState: setColumnSortState,
|
setColumnSortState: setColumnSortState,
|
||||||
|
columnsFilterStates: filterStates,
|
||||||
|
setColumnFilterState: setColumnFilterState,
|
||||||
...props,
|
...props,
|
||||||
}), [filteredColumns, sortState, setSortState, props]);
|
}), [sortState, setSortState, filterStates, setColumnFilterState, filteredColumns, props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext.Provider value={contextValue}>
|
<TableContext.Provider value={contextValue}>
|
||||||
|
@ -149,14 +197,26 @@ export interface TableContextData<CK extends ColumnKey> extends SmartablePropert
|
||||||
/**
|
/**
|
||||||
* Current table columns sort state.
|
* 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 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.
|
* @param sortType The sort type to set for the given column. NULL to reset sort state.
|
||||||
*/
|
*/
|
||||||
setColumnSortState: (key: CK, sortType: SortType|null) => void;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
79
src/Smartable/Sort.tsx
Normal file
79
src/Smartable/Sort.tsx
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
11
src/styles/_cells.less
Normal file
11
src/styles/_cells.less
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
td[role="button"]
|
||||||
|
{
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
{
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
30
src/styles/_filters.less
Normal file
30
src/styles/_filters.less
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
tr.filters
|
||||||
|
{ // Filters row style.
|
||||||
|
td
|
||||||
|
{
|
||||||
|
padding: 0.25em 0.125em;
|
||||||
|
|
||||||
|
&:first-child:not(:last-child)
|
||||||
|
{
|
||||||
|
padding-left: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, .select
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select
|
||||||
|
{
|
||||||
|
ul.selected
|
||||||
|
{
|
||||||
|
margin: 0 0 0.25em 0;
|
||||||
|
}
|
||||||
|
&.empty ul.selected
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,11 @@
|
||||||
tr.headings
|
tr.headings
|
||||||
|
{
|
||||||
|
&.disable-sort
|
||||||
|
{
|
||||||
|
th { pointer-events: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.disable-sort)
|
||||||
{
|
{
|
||||||
th
|
th
|
||||||
{
|
{
|
||||||
|
@ -60,5 +67,23 @@ tr.headings
|
||||||
{
|
{
|
||||||
bottom: 0.5em;
|
bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order
|
||||||
|
{ // Sort order indicator.
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
margin: auto 0;
|
||||||
|
padding: 0.3em 0.3em 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--foreground-lightest);
|
||||||
|
font-size: 0.7em;
|
||||||
|
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
src/styles/_loaders.less
Normal file
4
src/styles/_loaders.less
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
tr > td.generic.loader:first-child:last-child
|
||||||
|
{
|
||||||
|
height: 3em;
|
||||||
|
}
|
|
@ -1 +1,9 @@
|
||||||
|
table.smartable
|
||||||
|
{
|
||||||
|
margin: 0.5em auto;
|
||||||
|
|
||||||
|
@import "_cells";
|
||||||
|
@import "_filters";
|
||||||
@import "_headings";
|
@import "_headings";
|
||||||
|
@import "_loaders";
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||||
fileName: "index",
|
fileName: "index",
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ["react"],
|
external: ["react", "react-dom", "react-router-dom", "@phosphor-icons/react", "@kernelui/core"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue