Normalize async content handling with AsyncManager.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
+ Add AsyncManager which manages asynchronous table content for sorting and rendering.
This commit is contained in:
parent
792624fa73
commit
5ac1e4e7b8
4 changed files with 397 additions and 38 deletions
333
src/Smartable/AsyncManager.tsx
Normal file
333
src/Smartable/AsyncManager.tsx
Normal file
|
@ -0,0 +1,333 @@
|
|||
import React, {startTransition, useEffect, useRef, useState} from "react";
|
||||
import {ColumnKey} from "./Column";
|
||||
import {normalizeRowDefinition, 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]);
|
||||
|
||||
// Return the main instance of async manager.
|
||||
return asyncManager.current;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Smartable async data manager.
|
||||
*/
|
||||
class AsyncManager<CK extends ColumnKey>
|
||||
{
|
||||
currentDataState: CurrentTableData<CK>;
|
||||
setCurrentDataState: React.Dispatch<React.SetStateAction<CurrentTableData<CK>>>;
|
||||
|
||||
/**
|
||||
* Main promised data.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedData: Promised<SmartableData<CK>>;
|
||||
|
||||
/**
|
||||
* Promised rows data.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedRows: Promised<RowDefinition<CK>>[] = [];
|
||||
|
||||
/**
|
||||
* Promised rows cells data.
|
||||
* @protected
|
||||
*/
|
||||
protected promisedRowsCells: Partial<Record<CK, Promised<CellDefinition>>>[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* Rows data.
|
||||
* @protected
|
||||
*/
|
||||
protected rowsData: RowData<CK>[] = [];
|
||||
|
||||
/**
|
||||
* Rows cells definitions.
|
||||
* @protected
|
||||
*/
|
||||
protected cellsDefinitions: Partial<Record<CK, CellDefinition<CK>>>[] = [];
|
||||
|
||||
constructor()
|
||||
{
|
||||
// Initialize promised data object.
|
||||
this.promisedData = new Promised(this.handleNewRowsDefinitions.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new smartable data.
|
||||
* @param data Smartable data.
|
||||
*/
|
||||
handle(data: SmartableData<CK>): void
|
||||
{
|
||||
this.promisedData.refresh(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new rows definitions.
|
||||
* @param rowsDefinitions Rows definitions.
|
||||
* @protected
|
||||
*/
|
||||
protected handleNewRowsDefinitions(rowsDefinitions: Promisable<RowDefinition<CK>>[]): void
|
||||
{
|
||||
// Ignore undefined value.
|
||||
if (rowsDefinitions == undefined) return;
|
||||
|
||||
// Initialize rows data and cells definitions.
|
||||
this.rowsData = [];
|
||||
this.cellsDefinitions = [];
|
||||
|
||||
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.rowsData.length > 0 && this.cellsDefinitions.some((rowCells) => (
|
||||
Object.keys(rowCells).length > 0
|
||||
))
|
||||
)) // Nothing has changed.
|
||||
return;
|
||||
|
||||
// Initialize new data.
|
||||
const newData = {
|
||||
rows: [
|
||||
...(this.currentDataState?.rows ?? [])
|
||||
],
|
||||
};
|
||||
|
||||
for (const [rowId, newRow] of this.rowsData.entries())
|
||||
{ // Update value of each new row.
|
||||
newData.rows[rowId] = {
|
||||
element: newRow.element,
|
||||
cellElement: newRow.cellElement,
|
||||
cells: newData.rows[rowId]?.cells,
|
||||
};
|
||||
}
|
||||
|
||||
for (const [rowId, rowCells] of this.cellsDefinitions.entries())
|
||||
{ // Update cells of each changed row.
|
||||
newData.rows[rowId] = {
|
||||
...newData.rows[rowId],
|
||||
cells: {...(newData.rows[rowId]?.cells ?? {}), ...rowCells} as Record<CK, CellDefinition>,
|
||||
}
|
||||
}
|
||||
|
||||
// Update the current data state.
|
||||
this.currentDataState = newData;
|
||||
this.setCurrentDataState(
|
||||
newData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Promised data class.
|
||||
*/
|
||||
export class Promised<T>
|
||||
{
|
||||
/**
|
||||
* The main data promise.
|
||||
*/
|
||||
promise?: Promise<T>;
|
||||
|
||||
/**
|
||||
* Data retrieved from promise or given in parameter.
|
||||
*/
|
||||
data?: T;
|
||||
|
||||
/**
|
||||
* Called when data is changed.
|
||||
*/
|
||||
onData?: (data: T) => void;
|
||||
|
||||
constructor(onChanged?: (data: T) => void, data?: Promisable<T>)
|
||||
{
|
||||
this.onData = onChanged;
|
||||
if (data)
|
||||
this.refresh(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the promised data.
|
||||
* @param data Promised data.
|
||||
*/
|
||||
refresh(data: Promisable<T>): this
|
||||
{
|
||||
if (data instanceof Promise)
|
||||
{ // We have a promise of data.
|
||||
if (data != this.promise)
|
||||
{ // The promise is different from the saved one.
|
||||
// Save the new promise and set data to undefined.
|
||||
this.promise = data;
|
||||
this.data = undefined;
|
||||
this.onData?.(undefined);
|
||||
|
||||
// Wait for promise to resolve to get actual data.
|
||||
this.promise.then((data) => {
|
||||
// Data is retrieved, saving it.
|
||||
this.data = data;
|
||||
this.onData?.(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (data != this.data)
|
||||
{ // We already have data, and it is different from the current state.
|
||||
this.data = data;
|
||||
this.onData?.(data);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if some data (or its promise) have been provided.
|
||||
*/
|
||||
isInitialized(): boolean
|
||||
{
|
||||
return !!this.data || !!this.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if we are waiting for a promise result.
|
||||
*/
|
||||
isLoading(): boolean
|
||||
{
|
||||
return this.data === undefined && !!this.promise;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import React, {useContext} from "react";
|
|||
import {ColumnKey, useColumn} from "./Column";
|
||||
import {Smartable} from "./Smartable";
|
||||
import {useRow} from "./Row";
|
||||
import {SpinningLoader} from "@kernelui/core";
|
||||
|
||||
/**
|
||||
* Smartable cell definition.
|
||||
|
@ -67,3 +68,11 @@ export function CellInstance<T>({cell}: {cell: CellDefinition<T>})
|
|||
</CellContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated cell loader.
|
||||
*/
|
||||
export function CellLoader()
|
||||
{
|
||||
return <td className={"loading"}><SpinningLoader /></td>;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column";
|
||||
import {AutoColumnContextProvider, ColumnHeading, 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";
|
||||
|
||||
/**
|
||||
* Smartable instance component properties.
|
||||
|
@ -55,23 +55,17 @@ export function TableBody<CK extends ColumnKey>()
|
|||
// Get data from table.
|
||||
const {data} = useTable();
|
||||
|
||||
// Get current data state from the async table value.
|
||||
const {currentDataState} = useAsyncManager(data);
|
||||
|
||||
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>
|
||||
Array.isArray(currentDataState?.rows) ? (
|
||||
currentDataState.rows.map((rowData, index) => (
|
||||
// Rendering each row from its definition.
|
||||
<RowInstance row={rowData} />
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</Async>
|
||||
) : (
|
||||
<RowLoader />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, {useContext, useMemo} from "react";
|
||||
import React, {useContext} from "react";
|
||||
import {ColumnContext, ColumnKey} from "./Column";
|
||||
import {CellDefinition, CellInstance} from "./Cell";
|
||||
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>
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -75,11 +93,11 @@ export function RowCells()
|
|||
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>
|
||||
{
|
||||
row.cells?.[columnKey]
|
||||
? <CellInstance cell={row.cells?.[columnKey]} />
|
||||
: <CellLoader />
|
||||
}
|
||||
</ColumnContext.Provider>
|
||||
))
|
||||
);
|
||||
|
@ -88,21 +106,26 @@ 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()
|
||||
{
|
||||
return (
|
||||
<tr className={"generic loader"}></tr>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue