Compare commits

...

1 commit

Author SHA1 Message Date
76c56145ca
WIP
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-24 17:46:26 +02:00
7 changed files with 444 additions and 1 deletions

6
TODO.md Normal file
View file

@ -0,0 +1,6 @@
- [ ] Add shown columns.
- [ ] Test async content.
- [ ] Multi-columns sort.
- [ ] Pagination.
- [ ] Filters.
- [ ] Async filters.

View file

@ -1,10 +1,14 @@
import React from "react";
import {Application} from "@kernelui/core";
import {DemoTable} from "./DemoTable";
export function DemoApp()
{
return (
<Application>
<h1>Simple table</h1>
<DemoTable />
</Application>
)
}

71
demo/DemoTable.tsx Normal file
View file

@ -0,0 +1,71 @@
import React from "react";
import {createSmartable} from "../src/Smartable/Smartable";
import {createColumn, createColumns} from "../src/Smartable/Column";
// Create main table.
const Smartable = createSmartable({
columns: createColumns(
createColumn("123", {
title: "test",
}),
createColumn("456", {
title: "ttt",
}),
createColumn("789", {
title: "another",
}),
createColumn("test", {
title: "last one",
}),
),
});
export function DemoTable()
{
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",
},
},
]} />
);
}

View file

@ -0,0 +1,317 @@
import React, {useEffect, useRef, useState} from "react";
import {ColumnKey} from "./Column";
import {RowCells, RowData, RowDefinition} from "./Row";
import {CellDefinition} from "./Cell";
import {SmartableData} from "./Smartable";
import {Modify, Promisable} from "@kernelui/core";
/**
* Current Smartable data state.
*/
export interface CurrentTableData<CK extends ColumnKey>
{
/**
* Current rows state, undefined if they are still loading.
*/
rows?: CurrentRowData<CK>[];
}
/**
* Smartable current row cells data state.
*/
export type CurrentRowData<CK extends ColumnKey, T = any> = Modify<RowData<CK>, {
cells: Record<CK, CellDefinition<T>>;
}>;
/**
* Get current async table data manager.
*/
export function useAsyncManager<CK extends ColumnKey>(data: SmartableData<CK>): AsyncManager<CK>
{
// Get the main instance of async manager.
const asyncManager = useRef<AsyncManager<CK>>();
// Get the main async manager state.
const [currentDataState, setCurrentDataState] = useState<CurrentTableData<CK>>({
rows: undefined,
});
if (!asyncManager.current)
{
// Initialize a new async manager if there is none.
asyncManager.current = new AsyncManager<CK>();
}
// Update current data state and its dispatcher.
asyncManager.current.currentDataState = currentDataState;
asyncManager.current.setCurrentDataState = setCurrentDataState;
// When defined table data change, process async state again.
useEffect(() => {
// Process new data.
asyncManager.current.handle(data);
}, [asyncManager, data]);
//TODO is this required? Update the current async state.
//asyncManager.current.update();
// Return the main instance of async manager.
return asyncManager.current;
}
/**
* Smartable async data manager.
*/
class AsyncManager<CK extends ColumnKey>
{
currentDataState: CurrentTableData<CK>;
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
/**
* Data state update object.
*/
protected dataStateUpdate: {
rows: Record<number, Modify<RowData<CK>, {
cells: Partial<Record<CK, CellDefinition>>;
}>>;
};
/**
* Main promised data.
*/
protected promisedData: Promised<SmartableData<CK>>;
/**
* Promised row definitions.
*/
protected promisedRowsDefinitions: Promised<RowDefinition<CK>>[];
/**
* Promised full rows.
*/
protected promisedRows: Modify<RowData<CK>, {
cells: Record<CK, Promised<CellDefinition>>;
}>[];
constructor()
{
// Initialize promised data object.
this.promisedData = new Promised(this.handleNewData.bind(this));
this.promisedRows = [];
}
/**
* Handle new Smartable data.
* @param data Smartable data to handle.
*/
handle(data: SmartableData<CK>)
{
// Refresh global promised data.
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.
* @protected
*/
protected handleNewData(newData: (Promisable<RowDefinition<CK>>)[]): void
{
// Initialize a new array of updated promised rows.
const updatedPromiseRows: Promised<RowDefinition<CK>>[] = [];
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);
}
// 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>,
};
}
// 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();
}
/**
* 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>;
}
}
return newState;
}
}
export class Promised<T>
{
promise?: Promise<T>;
data?: T;
onData?: (data: T) => void;
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
{
this.onData = onChanged;
if (data)
this.refresh(data);
}
refresh(data: Promisable<T>): this
{
if (data instanceof Promise)
{
if (data != this.promise)
{
this.data = undefined;
this.promise = data;
this.onData?.(undefined);
this.promise.then((data) => {
this.data = data;
this.onData?.(data);
});
}
}
else
{
this.data = data;
this.onData?.(data);
}
return this;
}
isInitialized(): boolean
{
return !!this.data || !!this.promise;
}
isLoading(): boolean
{
return this.data === undefined && !!this.promise;
}
}

View file

@ -1,5 +1,6 @@
import React, {useCallback, useContext} from "react";
import {Smartable, useTable} from "./Smartable";
import {Instance} from "./Instance";
/**
* Basic column key type.
@ -36,6 +37,13 @@ export interface Column
* Column cell default element.
*/
cellElement?: React.ReactElement;
/**
* Sorting function for data of the column.
* @param a First data to compare.
* @param b Second data to compare.
*/
sort?: (a: unknown, b: unknown) => number;
}
/**
@ -163,3 +171,21 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
</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));
}

View file

@ -3,6 +3,7 @@ import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnK
import {SmartableProperties, useTable} from "./Smartable";
import {Async, Promisable} from "@kernelui/core";
import {RowDefinition, RowInstance} from "./Row";
import {useAsyncManager} from "./AsyncManager";
/**
* Smartable instance component properties.
@ -55,7 +56,21 @@ 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.
@ -73,5 +88,9 @@ export function TableBody<CK extends ColumnKey>()
</>
)}
</Async>
);
);*/
}
export function TableRows<CK extends ColumnKey>()
{
}

0
src/Utils.tsx Normal file
View file