Compare commits
1 commit
main
...
trash/asyn
Author | SHA1 | Date | |
---|---|---|---|
76c56145ca |
27 changed files with 777 additions and 1616 deletions
6
TODO.md
Normal file
6
TODO.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
- [ ] Add shown columns.
|
||||||
|
- [ ] Test async content.
|
||||||
|
- [ ] Multi-columns sort.
|
||||||
|
- [ ] Pagination.
|
||||||
|
- [ ] Filters.
|
||||||
|
- [ ] Async filters.
|
|
@ -1,17 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Application} from "@kernelui/core";
|
import {Application} from "@kernelui/core";
|
||||||
import {DemoTable} from "./DemoTable";
|
import {DemoTable} from "./DemoTable";
|
||||||
import {RemoveDemoTable} from "./RemoveDemoTable";
|
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Application>
|
<Application>
|
||||||
<h1>Random table</h1>
|
<h1>Simple table</h1>
|
||||||
|
|
||||||
<DemoTable />
|
<DemoTable />
|
||||||
|
|
||||||
<RemoveDemoTable />
|
|
||||||
</Application>
|
</Application>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,171 +1,71 @@
|
||||||
import React, {useMemo} from "react";
|
import React from "react";
|
||||||
import {createSmartable, SmartableColumns, SmartableData} from "../src/Smartable/Smartable";
|
import {createSmartable} from "../src/Smartable/Smartable";
|
||||||
import {createColumn, createColumns} from "../src/Smartable/Column";
|
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.
|
// Create main table.
|
||||||
const Smartable = createSmartable({
|
const Smartable = createSmartable({
|
||||||
columns: createColumns(
|
columns: createColumns(
|
||||||
createColumn("name", {
|
createColumn("123", {
|
||||||
title: "Name",
|
title: "test",
|
||||||
filter: StringFilter,
|
|
||||||
}),
|
}),
|
||||||
createColumn("quantity", {
|
createColumn("456", {
|
||||||
title: "Quantity",
|
title: "ttt",
|
||||||
filter: NumberFilter,
|
|
||||||
}),
|
}),
|
||||||
createColumn("unit-price", {
|
createColumn("789", {
|
||||||
title: "Unit price",
|
title: "another",
|
||||||
}),
|
}),
|
||||||
createColumn("total-price", {
|
createColumn("test", {
|
||||||
title: "Total",
|
title: "last one",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
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 (
|
return (
|
||||||
<Smartable.Table data={demoDataPromise} paginate={{ pageSize: 6 }} />
|
<Smartable.Table shownColumns={["123", "456", "test"]} data={[
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "test abc",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 123,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "test etset",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "test vccvcvc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "any data",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 5552,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "foo bar",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"123": {
|
||||||
|
data: "test test",
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
data: 5552,
|
||||||
|
},
|
||||||
|
"789": {
|
||||||
|
data: "other test",
|
||||||
|
},
|
||||||
|
"456": {
|
||||||
|
data: "infinite testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
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,18 +1,2 @@
|
||||||
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.1.1",
|
"version": "1.0.0-rc1",
|
||||||
"name": "@kernelui/smartable",
|
"name": "@kernelui/smartable",
|
||||||
"description": "Kernel UI Smartable.",
|
"description": "Kernel UI Smartable.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -16,23 +16,17 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"packageManager": "yarn@4.2.2"
|
||||||
"@kernelui/core": "^1.1.2"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.5.0"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 {RowCells, RowData, RowDefinition} from "./Row";
|
||||||
import {CellDefinition} from "./Cell";
|
import {CellDefinition} from "./Cell";
|
||||||
import {SmartableData} from "./Smartable";
|
import {SmartableData} from "./Smartable";
|
||||||
import {Modify, Promisable} from "@kernelui/core";
|
import {Modify, Promisable} from "@kernelui/core";
|
||||||
|
@ -53,6 +53,9 @@ export function useAsyncManager<CK extends ColumnKey>(data: SmartableData<CK>):
|
||||||
asyncManager.current.handle(data);
|
asyncManager.current.handle(data);
|
||||||
}, [asyncManager, data]);
|
}, [asyncManager, data]);
|
||||||
|
|
||||||
|
//TODO is this required? Update the current async state.
|
||||||
|
//asyncManager.current.update();
|
||||||
|
|
||||||
// Return the main instance of async manager.
|
// Return the main instance of async manager.
|
||||||
return asyncManager.current;
|
return asyncManager.current;
|
||||||
}
|
}
|
||||||
|
@ -66,236 +69,208 @@ class AsyncManager<CK extends ColumnKey>
|
||||||
currentDataState: CurrentTableData<CK>;
|
currentDataState: CurrentTableData<CK>;
|
||||||
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
|
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data state update object.
|
||||||
|
*/
|
||||||
|
protected dataStateUpdate: {
|
||||||
|
rows: Record<number, Modify<RowData<CK>, {
|
||||||
|
cells: Partial<Record<CK, CellDefinition>>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main promised data.
|
* Main promised data.
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
protected promisedData: Promised<SmartableData<CK>>;
|
protected promisedData: Promised<SmartableData<CK>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promised rows data.
|
* Promised row definitions.
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
protected promisedRows: Promised<RowDefinition<CK>>[] = [];
|
protected promisedRowsDefinitions: Promised<RowDefinition<CK>>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promised rows cells data.
|
* Promised full rows.
|
||||||
* @protected
|
|
||||||
*/
|
*/
|
||||||
protected promisedRowsCells: Partial<Record<CK, Promised<CellDefinition>>>[] = [];
|
protected promisedRows: Modify<RowData<CK>, {
|
||||||
|
cells: 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()
|
constructor()
|
||||||
{
|
{
|
||||||
// Initialize promised data object.
|
// Initialize promised data object.
|
||||||
this.promisedData = new Promised(this.handleNewRowsDefinitions.bind(this));
|
this.promisedData = new Promised(this.handleNewData.bind(this));
|
||||||
|
this.promisedRows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle new smartable data.
|
* Handle new Smartable data.
|
||||||
* @param data Smartable data.
|
* @param data Smartable data to handle.
|
||||||
*/
|
*/
|
||||||
handle(data: SmartableData<CK>): void
|
handle(data: SmartableData<CK>)
|
||||||
{
|
{
|
||||||
this.rowsLoaded = false;
|
// Refresh global promised data.
|
||||||
this.promisedData.refresh(data);
|
this.promisedData.refresh(data);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
console.log(this.dataStateUpdate);
|
||||||
* Handle new rows definitions.
|
// Update state.
|
||||||
* @param rowsDefinitions Rows definitions.
|
this.dataStateUpdate = {
|
||||||
* @protected
|
rows: this.dataStateUpdate?.rows,
|
||||||
*/
|
|
||||||
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 ?? [])
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
// Rows have been reinitialized.
|
/**
|
||||||
this.reinitRows = false;
|
* Called when new Smartable data is loaded.
|
||||||
|
* @param newData New loaded data.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewData(newData: (Promisable<RowDefinition<CK>>)[]): void
|
||||||
|
{
|
||||||
|
// Initialize a new array of updated promised rows.
|
||||||
|
const updatedPromiseRows: Promised<RowDefinition<CK>>[] = [];
|
||||||
|
|
||||||
for (const [rowId, newRow] of this.rowsData?.entries())
|
for (const [rowId, row] of newData.entries())
|
||||||
{ // Update value of each new row.
|
{ // For each promisable row, save the promised row in the updated array.
|
||||||
newData.rows[rowId] = {
|
updatedPromiseRows[rowId] = (this.promisedRowsDefinitions?.[rowId] ?? new Promised(this.handleNewRow.bind(this, rowId))).refresh(row);
|
||||||
element: newRow.element,
|
}
|
||||||
cellElement: newRow.cellElement,
|
|
||||||
cells: newData.rows[rowId]?.cells,
|
// Save new promised rows.
|
||||||
|
this.promisedRowsDefinitions = updatedPromiseRows;
|
||||||
|
|
||||||
|
// Update state.
|
||||||
|
this.dataStateUpdate = {
|
||||||
|
rows: this.dataStateUpdate.rows ?? undefined,
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new row definition is loaded.
|
||||||
|
* @param rowId Row ID.
|
||||||
|
* @param newRow New row definition.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewRow(rowId: number, newRow: RowDefinition<CK>): void
|
||||||
|
{
|
||||||
|
if (!("cells" in newRow) || !Object.values(newRow?.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.
|
||||||
|
newRow = {
|
||||||
|
cells: newRow as RowCells<CK>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [rowId, rowCells] of this.cellsDefinitions?.entries())
|
// Build the new row cells object, with the promised cells.
|
||||||
{ // Update cells of each changed row.
|
this.promisedRows[rowId] = Object.assign({}, newRow, {
|
||||||
newData.rows[rowId] = {
|
cells: Object.fromEntries(
|
||||||
...newData.rows[rowId],
|
// For each cell, create its promised object from the given promisable.
|
||||||
cells: {...(newData.rows[rowId]?.cells ?? {}), ...rowCells} as Record<CK, CellDefinition>,
|
(Object.entries(newRow.cells) as [CK, Promisable<CellDefinition>][]).map(([columnKey, cellData]) => (
|
||||||
|
// Return the same entry, with a promised instead of a promisable.
|
||||||
|
[columnKey, (this.promisedRows?.[rowId]?.cells?.[columnKey] ?? new Promised<CellDefinition>(this.handleNewCell.bind(this, rowId, columnKey))).refresh(cellData)]
|
||||||
|
))
|
||||||
|
) as Record<CK, Promised<CellDefinition>>,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update state.
|
||||||
|
this.dataStateUpdate = {
|
||||||
|
rows: {
|
||||||
|
[rowId]: {
|
||||||
|
cells: Object.fromEntries((Object.keys(newRow.cells) as CK[]).map((columnKey) => (
|
||||||
|
[columnKey, this.dataStateUpdate.rows[rowId].cells[columnKey]] ?? [columnKey, undefined]
|
||||||
|
))) as Record<CK, CellDefinition>,
|
||||||
|
cellElement: newRow.cellElement,
|
||||||
|
element: newRow.element,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new row cell definition is loaded.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected handleNewCell(rowId: number, columnKey: CK, cellData: CellDefinition): void
|
||||||
|
{
|
||||||
|
// Update state.
|
||||||
|
if (!this.dataStateUpdate?.rows)
|
||||||
|
this.dataStateUpdate = { rows: {} };
|
||||||
|
if (!this.dataStateUpdate.rows?.[rowId])
|
||||||
|
this.dataStateUpdate.rows[rowId] = { cells: {}, };
|
||||||
|
this.dataStateUpdate.rows[rowId].cells[columnKey] = cellData;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current async state.
|
||||||
|
*/
|
||||||
|
protected update(): void
|
||||||
|
{
|
||||||
|
// Set the new current state.
|
||||||
|
this.setCurrentDataState(this.currentDataState = this.buildNewState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a new state from the current async state.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected buildNewState(): CurrentTableData<CK>
|
||||||
|
{
|
||||||
|
if (this.promisedData.isInitialized())
|
||||||
|
{ // Waiting for initialization.
|
||||||
|
return this.currentDataState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
rows: this.currentDataState.rows,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.dataStateUpdate.rows)
|
||||||
|
{ // Something changed in the rows.
|
||||||
|
// Copy the existing rows, if there are some.
|
||||||
|
newState.rows = [...this.currentDataState.rows];
|
||||||
|
for (const [rowId, rowData] of Object.entries(this.dataStateUpdate.rows))
|
||||||
|
{ // For each changed row, creating its new state.
|
||||||
|
// Get current row state.
|
||||||
|
const currentRow = this.currentDataState.rows?.[parseInt(rowId)];
|
||||||
|
|
||||||
|
if (currentRow)
|
||||||
|
{ // If there was an existing row, copy the current row state and only change the specified cells.
|
||||||
|
const newRow = {...currentRow};
|
||||||
|
|
||||||
|
if (rowData.cells)
|
||||||
|
{ // If some cells have been changed, updating them.
|
||||||
|
newRow.cells = Object.assign({}, currentRow.cells, rowData.cells);
|
||||||
|
}
|
||||||
|
if (rowData.cellElement)
|
||||||
|
{ // If cell element have been changed, updating it.
|
||||||
|
newRow.cellElement = rowData.cellElement;
|
||||||
|
}
|
||||||
|
if (rowData.element)
|
||||||
|
{ // If element have been changed, updating it.
|
||||||
|
newRow.element = rowData.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new row state.
|
||||||
|
newState.rows[parseInt(rowId)] = newRow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// Create a new row state with the given cells' data.
|
||||||
|
newState.rows[parseInt(rowId)] = {...rowData} as CurrentRowData<CK>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the current data state.
|
return newState;
|
||||||
this.currentDataState = newData;
|
|
||||||
this.setCurrentDataState(
|
|
||||||
newData
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Promised data class.
|
|
||||||
*/
|
|
||||||
export class Promised<T>
|
export class Promised<T>
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The main data promise.
|
|
||||||
*/
|
|
||||||
promise?: Promise<T>;
|
promise?: Promise<T>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Data retrieved from promise or given in parameter.
|
|
||||||
*/
|
|
||||||
data?: T;
|
data?: T;
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when data is changed.
|
|
||||||
*/
|
|
||||||
onData?: (data: T) => void;
|
onData?: (data: T) => void;
|
||||||
|
|
||||||
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
|
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
|
||||||
|
@ -305,31 +280,24 @@ export class Promised<T>
|
||||||
this.refresh(data);
|
this.refresh(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the promised data.
|
|
||||||
* @param data Promised data.
|
|
||||||
*/
|
|
||||||
refresh(data: Promisable<T>): this
|
refresh(data: Promisable<T>): this
|
||||||
{
|
{
|
||||||
if (data instanceof Promise)
|
if (data instanceof Promise)
|
||||||
{ // We have a promise of data.
|
{
|
||||||
if (data != this.promise)
|
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.data = undefined;
|
||||||
|
this.promise = data;
|
||||||
this.onData?.(undefined);
|
this.onData?.(undefined);
|
||||||
|
|
||||||
// Wait for promise to resolve to get actual data.
|
|
||||||
this.promise.then((data) => {
|
this.promise.then((data) => {
|
||||||
// Data is retrieved, saving it.
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.onData?.(data);
|
this.onData?.(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (data != this.data)
|
else
|
||||||
{ // We already have data, and it is different from the current state.
|
{
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.onData?.(data);
|
this.onData?.(data);
|
||||||
}
|
}
|
||||||
|
@ -337,17 +305,11 @@ export class Promised<T>
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if some data (or its promise) have been provided.
|
|
||||||
*/
|
|
||||||
isInitialized(): boolean
|
isInitialized(): boolean
|
||||||
{
|
{
|
||||||
return !!this.data || !!this.promise;
|
return !!this.data || !!this.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if we are waiting for a promise result.
|
|
||||||
*/
|
|
||||||
isLoading(): boolean
|
isLoading(): boolean
|
||||||
{
|
{
|
||||||
return this.data === undefined && !!this.promise;
|
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, ...props}: React.PropsWithChildren<React.TdHTMLAttributes<HTMLTableCellElement>>)
|
export function Cell({children}: React.PropsWithChildren<{}>)
|
||||||
{
|
{
|
||||||
// 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 {...props}>{children ?? String(data)}</td>
|
<td>{children ?? String(data)}</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,11 +67,3 @@ export function CellInstance<T>({cell}: {cell: CellDefinition<T>})
|
||||||
</CellContext.Provider>
|
</CellContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Animated cell loader.
|
|
||||||
*/
|
|
||||||
export function CellLoader()
|
|
||||||
{
|
|
||||||
return <td className={"generic loader"}></td>;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
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,7 +1,6 @@
|
||||||
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 {Instance} from "./Instance";
|
||||||
import {SortState} from "./Sort";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic column key type.
|
* Basic column key type.
|
||||||
|
@ -27,7 +26,7 @@ export function createColumns<K extends ColumnKey>(...columns: [K, Column][]): C
|
||||||
/**
|
/**
|
||||||
* Smartable column definition.
|
* Smartable column definition.
|
||||||
*/
|
*/
|
||||||
export interface Column<T = any>
|
export interface Column
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Column title element.
|
* Column title element.
|
||||||
|
@ -40,16 +39,11 @@ export interface Column<T = any>
|
||||||
cellElement?: React.ReactElement;
|
cellElement?: React.ReactElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cells data comparison in the column.
|
* Sorting function for data of the column.
|
||||||
* @param a First data to compare.
|
* @param a First data to compare.
|
||||||
* @param b Second data to compare.
|
* @param b Second data to compare.
|
||||||
*/
|
*/
|
||||||
compare?: (a: T, b: T) => number;
|
sort?: (a: unknown, b: unknown) => number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Column filter definition.
|
|
||||||
*/
|
|
||||||
filter?: ColumnFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,6 +56,31 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -81,17 +100,6 @@ 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,6 +112,45 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -112,12 +159,6 @@ 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,
|
||||||
|
@ -125,12 +166,26 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global generic string comparator.
|
||||||
|
*/
|
||||||
|
const comparator = Intl.Collator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic sorting function for data of a column.
|
||||||
|
* @param a First data to compare.
|
||||||
|
* @param b Second data to compare.
|
||||||
|
*/
|
||||||
|
export function genericColumnSort(a: any, b: any): number
|
||||||
|
{
|
||||||
|
if (typeof a == "number" && typeof b == "number")
|
||||||
|
return b - a;
|
||||||
|
else
|
||||||
|
return comparator.compare(String(a), String(b));
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
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];
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
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} />
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
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} />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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,11 +1,9 @@
|
||||||
import React, {useMemo} from "react";
|
import React from "react";
|
||||||
import {AutoColumnContextProvider, Column, ColumnKey, Columns} from "./Column";
|
import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column";
|
||||||
import {SmartableProperties, useTable} from "./Smartable";
|
import {SmartableProperties, useTable} from "./Smartable";
|
||||||
import {RowInstance, RowLoader} from "./Row";
|
import {Async, Promisable} from "@kernelui/core";
|
||||||
|
import {RowDefinition, RowInstance} from "./Row";
|
||||||
import {useAsyncManager} from "./AsyncManager";
|
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.
|
||||||
|
@ -21,60 +19,15 @@ 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>(props: InstanceProperties<CK>)
|
export function Instance<CK extends ColumnKey>({columns}: 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 className={classes("smartable", className)}>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<ColumnsHeadings columns={columns} />
|
<ColumnsHeadings columns={columns} />
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{children}
|
<TableBody />
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
@ -85,86 +38,59 @@ export function Table<CK extends ColumnKey>({className, columns, children}: Reac
|
||||||
*/
|
*/
|
||||||
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}>
|
<ColumnHeading />
|
||||||
<ColumnHeading/>
|
</AutoColumnContextProvider>
|
||||||
</AutoColumnContextProvider>
|
))
|
||||||
))
|
|
||||||
}
|
|
||||||
</tr>
|
|
||||||
{ // Add filters if filter feature is not disabled.
|
|
||||||
disableFilter !== true && (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function TableBody<CK extends ColumnKey>()
|
||||||
* Smartable table body.
|
|
||||||
*/
|
|
||||||
export function TableBody<CK extends ColumnKey>({pagination}: {
|
|
||||||
/**
|
|
||||||
* Current pagination state.
|
|
||||||
*/
|
|
||||||
pagination?: { page: number; pageSize: number; };
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
// Get data from table.
|
// Get data from table.
|
||||||
const {data, columns, columnsSortState, columnsFilterStates} = useTable<CK>();
|
const {data} = useTable();
|
||||||
|
|
||||||
// Get current data state from the async table value.
|
// Get async data manager for the current table.
|
||||||
const {currentDataState} = useAsyncManager<CK>(data);
|
const asyncManager = useAsyncManager(data);
|
||||||
|
|
||||||
// Memorize filtered rows.
|
console.log(
|
||||||
const filteredRows = useMemo(() => (
|
asyncManager.currentDataState
|
||||||
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 (
|
return (
|
||||||
sortedRows ? (
|
asyncManager.currentDataState?.rows?.map((rowData, index) => (
|
||||||
sortedRows.map((rowData, index) => (
|
// Rendering each row.
|
||||||
// Rendering each row from its definition.
|
<RowInstance key={index} row={rowData} />
|
||||||
<RowInstance key={index} row={rowData} />
|
))
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<RowLoader />
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*return (
|
||||||
|
<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>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Async>
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableRows<CK extends ColumnKey>()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, {useContext} from "react";
|
import React, {useContext, useMemo} from "react";
|
||||||
import {AutoColumnContextProvider, ColumnContext, ColumnKey} from "./Column";
|
import {ColumnContext, ColumnKey} from "./Column";
|
||||||
import {CellDefinition, CellInstance, CellLoader} from "./Cell";
|
import {CellDefinition, CellInstance} from "./Cell";
|
||||||
import {Smartable, useTable} from "./Smartable";
|
import {Smartable, useTable} from "./Smartable";
|
||||||
import { Promisable} from "@kernelui/core";
|
import {Async, Promisable} from "@kernelui/core";
|
||||||
import {CurrentRowData} from "./AsyncManager";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smartable row cells.
|
* Smartable row cells.
|
||||||
|
@ -36,27 +35,10 @@ 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 CurrentRowData<CK, T>
|
export interface RowContextData<CK extends ColumnKey, T = any> extends RowData<CK, T>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +73,14 @@ export function RowCells()
|
||||||
const {columns} = useTable();
|
const {columns} = useTable();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Object.keys(columns).map((columnKey) => (
|
Object.entries(columns).map(([columnKey, column]) => (
|
||||||
<AutoColumnContextProvider key={columnKey} columnKey={columnKey}>
|
<ColumnContext.Provider key={columnKey} value={{ key: columnKey, column: column }}>
|
||||||
{ // Show current cell.
|
<Async<CellDefinition> promise={row.cells?.[columnKey] ?? { data: undefined }}>
|
||||||
row.cells?.[columnKey]
|
{(cellDefinition) => (
|
||||||
? <CellInstance cell={row.cells?.[columnKey]} />
|
<CellInstance cell={cellDefinition} />
|
||||||
: <CellLoader />
|
)}
|
||||||
}
|
</Async>
|
||||||
</AutoColumnContextProvider>
|
</ColumnContext.Provider>
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -106,29 +88,21 @@ export function RowCells()
|
||||||
/**
|
/**
|
||||||
* Row instance component.
|
* Row instance component.
|
||||||
*/
|
*/
|
||||||
export function RowInstance<CK extends ColumnKey>({row}: { row: CurrentRowData<CK> })
|
export function RowInstance<CK extends ColumnKey>({row}: { row: RowDefinition<CK> })
|
||||||
{
|
{
|
||||||
// Get table row element.
|
// Get row context value from given row definition.
|
||||||
|
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={row}>
|
<RowContext.Provider value={rowContextValue}>
|
||||||
{ // 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.
|
||||||
row.element ?? rowElement ?? <Row />
|
rowContextValue.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,9 +1,8 @@
|
||||||
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} from "./Column";
|
import {ColumnKey, Columns, SortState, SortType} 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.
|
||||||
|
@ -15,11 +14,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -39,26 +33,6 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,11 +45,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
@ -112,7 +81,10 @@ 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 Partial<Record<CK, SortState>>);
|
const [sortState, setSortState] = useState({} as 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 => {
|
||||||
|
@ -146,33 +118,13 @@ 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,
|
||||||
}), [sortState, setSortState, filterStates, setColumnFilterState, filteredColumns, props]);
|
}), [filteredColumns, sortState, setSortState, props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext.Provider value={contextValue}>
|
<TableContext.Provider value={contextValue}>
|
||||||
|
@ -197,26 +149,14 @@ export interface TableContextData<CK extends ColumnKey> extends SmartablePropert
|
||||||
/**
|
/**
|
||||||
* Current table columns sort state.
|
* Current table columns sort state.
|
||||||
*/
|
*/
|
||||||
columnsSortState: Partial<Record<CK, SortState>>;
|
columnsSortState: Record<CK, SortState>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set given table column sort state.
|
* Set current table columns 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
0
src/Utils.tsx
Normal file
0
src/Utils.tsx
Normal file
|
@ -1,11 +0,0 @@
|
||||||
td[role="button"]
|
|
||||||
{
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
{
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
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,89 +1,64 @@
|
||||||
tr.headings
|
tr.headings
|
||||||
{
|
{
|
||||||
&.disable-sort
|
th
|
||||||
{
|
{
|
||||||
th { pointer-events: none; }
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.disable-sort)
|
cursor: pointer;
|
||||||
{
|
|
||||||
th
|
&::before, &::after
|
||||||
|
{ // Sorting order indicator.
|
||||||
|
transition: height 0.2s ease, background 0.2s ease, top 0.2s ease, bottom 0.2s ease;
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background: var(--background-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before
|
||||||
{
|
{
|
||||||
position: relative;
|
right: calc(0.33em - 1px);
|
||||||
|
|
||||||
cursor: pointer;
|
width: 2px;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
&::after
|
||||||
|
{
|
||||||
|
right: calc(0.33em - 3px);
|
||||||
|
|
||||||
&::before, &::after
|
width: 6px;
|
||||||
{ // Sorting order indicator.
|
height: 6px;
|
||||||
transition: height 0.2s ease, background 0.2s ease, top 0.2s ease, bottom 0.2s ease;
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
content: "";
|
&.asc, &.desc
|
||||||
position: absolute;
|
{
|
||||||
top: 0;
|
&::after, &::before
|
||||||
bottom: 0;
|
{
|
||||||
|
background: var(--primary);
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
background: var(--background-darkest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before
|
&::before
|
||||||
{
|
{
|
||||||
right: calc(0.33em - 1px);
|
height: 0.8em;
|
||||||
|
|
||||||
width: 2px;
|
|
||||||
height: 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
&::after
|
}
|
||||||
{
|
|
||||||
right: calc(0.33em - 3px);
|
|
||||||
|
|
||||||
width: 6px;
|
&.asc::after
|
||||||
height: 6px;
|
{
|
||||||
border-radius: 6px;
|
top: 0.5em;
|
||||||
}
|
}
|
||||||
|
&.desc::after
|
||||||
&.asc, &.desc
|
{
|
||||||
{
|
bottom: 0.5em;
|
||||||
&::after, &::before
|
|
||||||
{
|
|
||||||
background: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before
|
|
||||||
{
|
|
||||||
height: 0.8em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.asc::after
|
|
||||||
{
|
|
||||||
top: 0.5em;
|
|
||||||
}
|
|
||||||
&.desc::after
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
tr > td.generic.loader:first-child:last-child
|
|
||||||
{
|
|
||||||
height: 3em;
|
|
||||||
}
|
|
|
@ -1,9 +1 @@
|
||||||
table.smartable
|
@import "_headings";
|
||||||
{
|
|
||||||
margin: 0.5em auto;
|
|
||||||
|
|
||||||
@import "_cells";
|
|
||||||
@import "_filters";
|
|
||||||
@import "_headings";
|
|
||||||
@import "_loaders";
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||||
fileName: "index",
|
fileName: "index",
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ["react", "react-dom", "react-router-dom", "@phosphor-icons/react", "@kernelui/core"],
|
external: ["react"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue