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 |
27 changed files with 1589 additions and 750 deletions
6
TODO.md
6
TODO.md
|
@ -1,6 +0,0 @@
|
|||
- [ ] Add shown columns.
|
||||
- [ ] Test async content.
|
||||
- [ ] Multi-columns sort.
|
||||
- [ ] Pagination.
|
||||
- [ ] Filters.
|
||||
- [ ] Async filters.
|
|
@ -1,14 +1,17 @@
|
|||
import React from "react";
|
||||
import {Application} from "@kernelui/core";
|
||||
import {DemoTable} from "./DemoTable";
|
||||
import {RemoveDemoTable} from "./RemoveDemoTable";
|
||||
|
||||
export function DemoApp()
|
||||
{
|
||||
return (
|
||||
<Application>
|
||||
<h1>Simple table</h1>
|
||||
<h1>Random table</h1>
|
||||
|
||||
<DemoTable />
|
||||
|
||||
<RemoveDemoTable />
|
||||
</Application>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,71 +1,171 @@
|
|||
import React from "react";
|
||||
import {createSmartable} from "../src/Smartable/Smartable";
|
||||
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("123", {
|
||||
title: "test",
|
||||
createColumn("name", {
|
||||
title: "Name",
|
||||
filter: StringFilter,
|
||||
}),
|
||||
createColumn("456", {
|
||||
title: "ttt",
|
||||
createColumn("quantity", {
|
||||
title: "Quantity",
|
||||
filter: NumberFilter,
|
||||
}),
|
||||
createColumn("789", {
|
||||
title: "another",
|
||||
createColumn("unit-price", {
|
||||
title: "Unit price",
|
||||
}),
|
||||
createColumn("test", {
|
||||
title: "last one",
|
||||
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 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",
|
||||
},
|
||||
},
|
||||
]} />
|
||||
<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";
|
||||
|
||||
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",
|
||||
"description": "Kernel UI Smartable.",
|
||||
"scripts": {
|
||||
|
@ -16,17 +16,23 @@
|
|||
"publishConfig": {
|
||||
"@kernelui:registry": "https://code.zeptotech.net/api/packages/UIKernel/npm/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kernelui/core": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kernelui/core": "^1.6.0",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"less": "^4.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-dts": "^3.9.1"
|
||||
},
|
||||
"packageManager": "yarn@4.2.2"
|
||||
"peerDependencies": {
|
||||
"@kernelui/core": "^1.1.2"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {ColumnKey} from "./Column";
|
||||
import {RowCells, RowData, RowDefinition} from "./Row";
|
||||
import {normalizeRowDefinition, RowData, RowDefinition} from "./Row";
|
||||
import {CellDefinition} from "./Cell";
|
||||
import {SmartableData} from "./Smartable";
|
||||
import {Modify, Promisable} from "@kernelui/core";
|
||||
|
@ -53,9 +53,6 @@ export function useAsyncManager<CK extends ColumnKey>(data: SmartableData<CK>):
|
|||
asyncManager.current.handle(data);
|
||||
}, [asyncManager, data]);
|
||||
|
||||
//TODO is this required? Update the current async state.
|
||||
//asyncManager.current.update();
|
||||
|
||||
// Return the main instance of async manager.
|
||||
return asyncManager.current;
|
||||
}
|
||||
|
@ -69,208 +66,236 @@ class AsyncManager<CK extends ColumnKey>
|
|||
currentDataState: 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.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedData: Promised<SmartableData<CK>>;
|
||||
|
||||
/**
|
||||
* Promised row definitions.
|
||||
* Promised rows data.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedRowsDefinitions: Promised<RowDefinition<CK>>[];
|
||||
protected promisedRows: Promised<RowDefinition<CK>>[] = [];
|
||||
|
||||
/**
|
||||
* Promised full rows.
|
||||
* Promised rows cells data.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedRows: Modify<RowData<CK>, {
|
||||
cells: Record<CK, Promised<CellDefinition>>;
|
||||
}>[];
|
||||
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.handleNewData.bind(this));
|
||||
this.promisedRows = [];
|
||||
this.promisedData = new Promised(this.handleNewRowsDefinitions.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new Smartable data.
|
||||
* @param data Smartable data to handle.
|
||||
* Handle new smartable data.
|
||||
* @param data Smartable data.
|
||||
*/
|
||||
handle(data: SmartableData<CK>)
|
||||
handle(data: SmartableData<CK>): void
|
||||
{
|
||||
// Refresh global promised data.
|
||||
this.rowsLoaded = false;
|
||||
this.promisedData.refresh(data);
|
||||
|
||||
console.log(this.dataStateUpdate);
|
||||
// Update state.
|
||||
this.dataStateUpdate = {
|
||||
rows: this.dataStateUpdate?.rows,
|
||||
};
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when new Smartable data is loaded.
|
||||
* @param newData New loaded data.
|
||||
* Handle new rows definitions.
|
||||
* @param rowsDefinitions Rows definitions.
|
||||
* @protected
|
||||
*/
|
||||
protected handleNewData(newData: (Promisable<RowDefinition<CK>>)[]): void
|
||||
protected handleNewRowsDefinitions(rowsDefinitions: Promisable<RowDefinition<CK>>[]): void
|
||||
{
|
||||
// Initialize a new array of updated promised rows.
|
||||
const updatedPromiseRows: Promised<RowDefinition<CK>>[] = [];
|
||||
// Ignore undefined value.
|
||||
if (rowsDefinitions == undefined) return;
|
||||
|
||||
for (const [rowId, row] of newData.entries())
|
||||
{ // For each promisable row, save the promised row in the updated array.
|
||||
updatedPromiseRows[rowId] = (this.promisedRowsDefinitions?.[rowId] ?? new Promised(this.handleNewRow.bind(this, rowId))).refresh(row);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Save new promised rows.
|
||||
this.promisedRowsDefinitions = updatedPromiseRows;
|
||||
|
||||
// Update state.
|
||||
this.dataStateUpdate = {
|
||||
rows: this.dataStateUpdate.rows ?? undefined,
|
||||
};
|
||||
this.update();
|
||||
// Try to update the current data state.
|
||||
this.tryToUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new row definition is loaded.
|
||||
* Handle a new row.
|
||||
* @param rowId Row ID.
|
||||
* @param newRow New row definition.
|
||||
* @param row Row definition.
|
||||
* @protected
|
||||
*/
|
||||
protected handleNewRow(rowId: number, newRow: RowDefinition<CK>): void
|
||||
protected handleNewRow(rowId: number, row: 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>,
|
||||
};
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Build the new row cells object, with the promised cells.
|
||||
this.promisedRows[rowId] = Object.assign({}, newRow, {
|
||||
cells: Object.fromEntries(
|
||||
// For each cell, create its promised object from the given promisable.
|
||||
(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();
|
||||
// Try to update the current data state.
|
||||
this.tryToUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new row cell definition is loaded.
|
||||
* 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, cellData: CellDefinition): void
|
||||
protected handleNewCell(rowId: number, columnKey: CK, cellDefinition: 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();
|
||||
// Ignore undefined value.
|
||||
if (cellDefinition == undefined) return;
|
||||
|
||||
// Save cell definition.
|
||||
this.cellsDefinitions[rowId][columnKey] = cellDefinition;
|
||||
|
||||
// Try to update the current data state.
|
||||
this.tryToUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current async state.
|
||||
* 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
|
||||
{
|
||||
// Set the new current state.
|
||||
this.setCurrentDataState(this.currentDataState = this.buildNewState());
|
||||
}
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
// Initialize new data.
|
||||
const newData = {
|
||||
rows: !this.rowsLoaded ? undefined : this.reinitRows ? [] : [
|
||||
...(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)];
|
||||
// Rows have been reinitialized.
|
||||
this.reinitRows = false;
|
||||
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// 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>;
|
||||
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>,
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
// 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>)
|
||||
|
@ -280,24 +305,31 @@ export class Promised<T>
|
|||
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)
|
||||
{
|
||||
this.data = undefined;
|
||||
{ // 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
|
||||
{
|
||||
else if (data != this.data)
|
||||
{ // We already have data, and it is different from the current state.
|
||||
this.data = data;
|
||||
this.onData?.(data);
|
||||
}
|
||||
|
@ -305,11 +337,17 @@ export class Promised<T>
|
|||
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.
|
||||
*/
|
||||
export function Cell({children}: React.PropsWithChildren<{}>)
|
||||
export function Cell({children, ...props}: React.PropsWithChildren<React.TdHTMLAttributes<HTMLTableCellElement>>)
|
||||
{
|
||||
// Get cell data.
|
||||
const {data} = useCell();
|
||||
|
||||
// Try to render cell data to string when no children given.
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,6 +1,7 @@
|
|||
import React, {useCallback, useContext} from "react";
|
||||
import {Smartable, useTable} from "./Smartable";
|
||||
import {Instance} from "./Instance";
|
||||
import {ColumnFilter} from "./Columns/ColumnFilter";
|
||||
import {SortState} from "./Sort";
|
||||
|
||||
/**
|
||||
* Basic column key type.
|
||||
|
@ -26,7 +27,7 @@ export function createColumns<K extends ColumnKey>(...columns: [K, Column][]): C
|
|||
/**
|
||||
* Smartable column definition.
|
||||
*/
|
||||
export interface Column
|
||||
export interface Column<T = any>
|
||||
{
|
||||
/**
|
||||
* Column title element.
|
||||
|
@ -39,11 +40,16 @@ export interface Column
|
|||
cellElement?: React.ReactElement;
|
||||
|
||||
/**
|
||||
* Sorting function for data of the column.
|
||||
* Cells data comparison in the column.
|
||||
* @param a First data to compare.
|
||||
* @param b Second data to compare.
|
||||
*/
|
||||
sort?: (a: unknown, b: unknown) => number;
|
||||
compare?: (a: T, b: T) => number;
|
||||
|
||||
/**
|
||||
* Column filter definition.
|
||||
*/
|
||||
filter?: ColumnFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,31 +62,6 @@ export function createColumn<K extends ColumnKey>(key: K, column: Column): [K, C
|
|||
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.
|
||||
*/
|
||||
|
@ -100,6 +81,17 @@ export interface ColumnContextData<CK extends ColumnKey>
|
|||
* Column sort state.
|
||||
*/
|
||||
sortState?: SortState;
|
||||
|
||||
/**
|
||||
* Column filter state.
|
||||
*/
|
||||
filterState: any;
|
||||
|
||||
/**
|
||||
* Set current column filter state.
|
||||
* @param filterState New filter state.
|
||||
*/
|
||||
setFilterState: <T = any>(filterState: T) => void;
|
||||
}
|
||||
|
||||
export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined);
|
||||
|
@ -112,45 +104,6 @@ export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): Colu
|
|||
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.
|
||||
*/
|
||||
|
@ -159,6 +112,12 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
|||
// Get table data.
|
||||
const table = useTable();
|
||||
|
||||
// Initialize filterState dispatcher for the current column.
|
||||
const setFilterState = useCallback((filterState: any) => (
|
||||
// Set the filter state for the current column key.
|
||||
table.setColumnFilterState(columnKey, filterState)
|
||||
), [columnKey, table.setColumnFilterState]);
|
||||
|
||||
return (
|
||||
<ColumnContext.Provider value={{
|
||||
key: columnKey,
|
||||
|
@ -166,26 +125,12 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
|
|||
column: table.columns[columnKey],
|
||||
// Get current column sort state from table data.
|
||||
sortState: table.columnsSortState?.[columnKey],
|
||||
// Get current column filter state from table data.
|
||||
filterState: table.columnsFilterStates?.[columnKey],
|
||||
// Current column filter state dispatcher.
|
||||
setFilterState: setFilterState,
|
||||
}}>
|
||||
{children}
|
||||
</ColumnContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
|
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,9 +1,11 @@
|
|||
import React from "react";
|
||||
import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column";
|
||||
import React, {useMemo} from "react";
|
||||
import {AutoColumnContextProvider, Column, ColumnKey, Columns} from "./Column";
|
||||
import {SmartableProperties, useTable} from "./Smartable";
|
||||
import {Async, Promisable} from "@kernelui/core";
|
||||
import {RowDefinition, RowInstance} from "./Row";
|
||||
import {RowInstance, RowLoader} 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.
|
||||
|
@ -19,15 +21,60 @@ export interface InstanceProperties<CK extends ColumnKey> extends SmartablePrope
|
|||
/**
|
||||
* 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 (
|
||||
<table>
|
||||
<table className={classes("smartable", className)}>
|
||||
<thead>
|
||||
<ColumnsHeadings columns={columns} />
|
||||
</thead>
|
||||
<tbody>
|
||||
<TableBody />
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
@ -38,59 +85,86 @@ export function Instance<CK extends ColumnKey>({columns}: InstanceProperties<CK>
|
|||
*/
|
||||
export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Columns<CK>})
|
||||
{
|
||||
// Get feature disable options.
|
||||
const {disableSort, disableFilter} = useTable<CK>();
|
||||
|
||||
return (
|
||||
<tr className={"headings"}>
|
||||
<>
|
||||
<tr className={classes("headings", disableSort === true ? "disable-sort" : undefined)}>
|
||||
{ // Showing title of each column.
|
||||
Object.keys(columns).map((key) => (
|
||||
<AutoColumnContextProvider key={key} columnKey={key}>
|
||||
<ColumnHeading />
|
||||
<ColumnHeading/>
|
||||
</AutoColumnContextProvider>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableBody<CK extends ColumnKey>()
|
||||
{
|
||||
// Get data from table.
|
||||
const {data} = useTable();
|
||||
|
||||
// Get async data manager for the current table.
|
||||
const asyncManager = useAsyncManager(data);
|
||||
|
||||
console.log(
|
||||
asyncManager.currentDataState
|
||||
);
|
||||
|
||||
return (
|
||||
asyncManager.currentDataState?.rows?.map((rowData, index) => (
|
||||
// Rendering each row.
|
||||
<RowInstance key={index} row={rowData} />
|
||||
))
|
||||
);
|
||||
|
||||
/*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>
|
||||
{ // 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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Async>
|
||||
);*/
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRows<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.
|
||||
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 {ColumnContext, ColumnKey} from "./Column";
|
||||
import {CellDefinition, CellInstance} from "./Cell";
|
||||
import React, {useContext} from "react";
|
||||
import {AutoColumnContextProvider, ColumnContext, ColumnKey} from "./Column";
|
||||
import {CellDefinition, CellInstance, CellLoader} from "./Cell";
|
||||
import {Smartable, useTable} from "./Smartable";
|
||||
import {Async, Promisable} from "@kernelui/core";
|
||||
import { Promisable} from "@kernelui/core";
|
||||
import {CurrentRowData} from "./AsyncManager";
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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();
|
||||
|
||||
return (
|
||||
Object.entries(columns).map(([columnKey, column]) => (
|
||||
<ColumnContext.Provider key={columnKey} value={{ key: columnKey, column: column }}>
|
||||
<Async<CellDefinition> promise={row.cells?.[columnKey] ?? { data: undefined }}>
|
||||
{(cellDefinition) => (
|
||||
<CellInstance cell={cellDefinition} />
|
||||
)}
|
||||
</Async>
|
||||
</ColumnContext.Provider>
|
||||
Object.keys(columns).map((columnKey) => (
|
||||
<AutoColumnContextProvider key={columnKey} columnKey={columnKey}>
|
||||
{ // Show current cell.
|
||||
row.cells?.[columnKey]
|
||||
? <CellInstance cell={row.cells?.[columnKey]} />
|
||||
: <CellLoader />
|
||||
}
|
||||
</AutoColumnContextProvider>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
@ -88,21 +106,29 @@ export function RowCells()
|
|||
/**
|
||||
* 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.
|
||||
const rowContextValue = useMemo(() => (
|
||||
// If a simple RowCells<CK> object is given, converting it to a RowData<CK>
|
||||
!("cells" in row) ? { cells: row } : row
|
||||
), [row]);
|
||||
|
||||
// Get table row element.
|
||||
const {rowElement} = useTable();
|
||||
|
||||
return (
|
||||
<RowContext.Provider value={rowContextValue}>
|
||||
<RowContext.Provider value={row}>
|
||||
{ // Trying to render row-specific element, then table-specific element, then default element.
|
||||
rowContextValue.element ?? rowElement ?? <Row />
|
||||
row.element ?? rowElement ?? <Row />
|
||||
}
|
||||
</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 {Instance} from "./Instance";
|
||||
import {ColumnKey, Columns, SortState, SortType} from "./Column";
|
||||
import {ColumnKey, Columns} from "./Column";
|
||||
import {RowDefinition} from "./Row";
|
||||
import {Promisable} from "@kernelui/core";
|
||||
import {SortState, SortType} from "./Sort";
|
||||
|
||||
/**
|
||||
* Smartable data type.
|
||||
|
@ -14,6 +15,11 @@ export type SmartableData<CK extends ColumnKey> = Promisable<(Promisable<RowDefi
|
|||
*/
|
||||
export interface SmartableProperties<CK extends ColumnKey>
|
||||
{
|
||||
/**
|
||||
* Table custom class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Table data.
|
||||
*/
|
||||
|
@ -33,6 +39,26 @@ export interface SmartableProperties<CK extends ColumnKey>
|
|||
* Default column heading element.
|
||||
*/
|
||||
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>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Smartable columns infered type.
|
||||
*/
|
||||
export type SmartableColumns<CK> = CK extends Smartable<infer CK> ? CK : never;
|
||||
|
||||
/**
|
||||
* Define a new Smartable.
|
||||
*/
|
||||
|
@ -81,10 +112,7 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
|||
return {
|
||||
Table: (props: SmartableProperties<CK>) => {
|
||||
// Initialize sort state.
|
||||
const [sortState, setSortState] = useState({} as Record<CK, SortState>);
|
||||
|
||||
// Filter columns from the given property.
|
||||
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
|
||||
const [sortState, setSortState] = useState({} as Partial<Record<CK, SortState>>);
|
||||
|
||||
// Set sort state of a specific column.
|
||||
const setColumnSortState = useCallback((key: CK, sortType: SortType|null): void => {
|
||||
|
@ -118,13 +146,33 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
|
|||
setSortState(newSortState);
|
||||
}, [sortState, setSortState]);
|
||||
|
||||
// Initialize filter states.
|
||||
const [filterStates, setFilterStates] = useState<Partial<Record<CK, any>>>({});
|
||||
|
||||
// Set filter state of a specific column.
|
||||
const setColumnFilterState = useCallback((key: CK, filterState: any) => {
|
||||
setFilterStates(
|
||||
{
|
||||
// Copy the other filters states.
|
||||
...filterStates,
|
||||
// Set the filter state for the given column.
|
||||
[key]: filterState,
|
||||
}
|
||||
);
|
||||
}, [filterStates, setFilterStates]);
|
||||
|
||||
// Filter columns from the given property.
|
||||
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
|
||||
|
||||
// Initialize table context value.
|
||||
const contextValue = useMemo<TableContextData<CK>>(() => ({
|
||||
columns: filteredColumns,
|
||||
columnsSortState: sortState,
|
||||
setColumnSortState: setColumnSortState,
|
||||
columnsFilterStates: filterStates,
|
||||
setColumnFilterState: setColumnFilterState,
|
||||
...props,
|
||||
}), [filteredColumns, sortState, setSortState, props]);
|
||||
}), [sortState, setSortState, filterStates, setColumnFilterState, filteredColumns, props]);
|
||||
|
||||
return (
|
||||
<TableContext.Provider value={contextValue}>
|
||||
|
@ -149,14 +197,26 @@ export interface TableContextData<CK extends ColumnKey> extends SmartablePropert
|
|||
/**
|
||||
* Current table columns sort state.
|
||||
*/
|
||||
columnsSortState: Record<CK, SortState>;
|
||||
columnsSortState: Partial<Record<CK, SortState>>;
|
||||
|
||||
/**
|
||||
* Set current table columns sort state.
|
||||
* Set given table column sort state.
|
||||
* @param key The column key for which to set the sort type.
|
||||
* @param sortType The sort type to set for the given column. NULL to reset sort state.
|
||||
*/
|
||||
setColumnSortState: (key: CK, sortType: SortType|null) => void;
|
||||
|
||||
/**
|
||||
* Current table columsn filter states.
|
||||
*/
|
||||
columnsFilterStates: Partial<Record<CK, any>>;
|
||||
|
||||
/**
|
||||
* Set given table column filter state.
|
||||
* @param key The column key for which to set the filter state.
|
||||
* @param filterState The filter state to set for the given column.
|
||||
*/
|
||||
setColumnFilterState: (key: CK, filterState: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
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,5 +1,12 @@
|
|||
tr.headings
|
||||
{
|
||||
&.disable-sort
|
||||
{
|
||||
th { pointer-events: none; }
|
||||
}
|
||||
|
||||
&:not(.disable-sort)
|
||||
{
|
||||
th
|
||||
{
|
||||
position: relative;
|
||||
|
@ -60,5 +67,23 @@ tr.headings
|
|||
{
|
||||
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 @@
|
|||
@import "_headings";
|
||||
table.smartable
|
||||
{
|
||||
margin: 0.5em auto;
|
||||
|
||||
@import "_cells";
|
||||
@import "_filters";
|
||||
@import "_headings";
|
||||
@import "_loaders";
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
fileName: "index",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["react"],
|
||||
external: ["react", "react-dom", "react-router-dom", "@phosphor-icons/react", "@kernelui/core"],
|
||||
},
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue