From 76c56145cacc01ae4c1a482eaa0af381edd36362 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Wed, 24 Jul 2024 17:46:26 +0200 Subject: [PATCH] WIP --- TODO.md | 6 + demo/DemoApp.tsx | 4 + demo/DemoTable.tsx | 71 ++++++++ src/Smartable/AsyncManager.tsx | 317 +++++++++++++++++++++++++++++++++ src/Smartable/Column.tsx | 26 +++ src/Smartable/Instance.tsx | 21 ++- src/Utils.tsx | 0 7 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 TODO.md create mode 100644 demo/DemoTable.tsx create mode 100644 src/Smartable/AsyncManager.tsx create mode 100644 src/Utils.tsx diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..1875848 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +- [ ] Add shown columns. +- [ ] Test async content. +- [ ] Multi-columns sort. +- [ ] Pagination. +- [ ] Filters. +- [ ] Async filters. diff --git a/demo/DemoApp.tsx b/demo/DemoApp.tsx index 115918b..3447b55 100644 --- a/demo/DemoApp.tsx +++ b/demo/DemoApp.tsx @@ -1,10 +1,14 @@ import React from "react"; import {Application} from "@kernelui/core"; +import {DemoTable} from "./DemoTable"; export function DemoApp() { return ( +

Simple table

+ +
) } diff --git a/demo/DemoTable.tsx b/demo/DemoTable.tsx new file mode 100644 index 0000000..e8f4ea2 --- /dev/null +++ b/demo/DemoTable.tsx @@ -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 ( + + ); +} diff --git a/src/Smartable/AsyncManager.tsx b/src/Smartable/AsyncManager.tsx new file mode 100644 index 0000000..dd3729a --- /dev/null +++ b/src/Smartable/AsyncManager.tsx @@ -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 +{ + /** + * Current rows state, undefined if they are still loading. + */ + rows?: CurrentRowData[]; +} + +/** + * Smartable current row cells data state. + */ +export type CurrentRowData = Modify, { + cells: Record>; +}>; + + +/** + * Get current async table data manager. + */ +export function useAsyncManager(data: SmartableData): AsyncManager +{ + // Get the main instance of async manager. + const asyncManager = useRef>(); + + // Get the main async manager state. + const [currentDataState, setCurrentDataState] = useState>({ + rows: undefined, + }); + + if (!asyncManager.current) + { + // Initialize a new async manager if there is none. + asyncManager.current = new AsyncManager(); + } + + // 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 +{ + currentDataState: CurrentTableData; + setCurrentDataState: React.Dispatch>>; + + + /** + * Data state update object. + */ + protected dataStateUpdate: { + rows: Record, { + cells: Partial>; + }>>; + }; + + /** + * Main promised data. + */ + protected promisedData: Promised>; + + /** + * Promised row definitions. + */ + protected promisedRowsDefinitions: Promised>[]; + + /** + * Promised full rows. + */ + protected promisedRows: Modify, { + cells: Record>; + }>[]; + + 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) + { + // 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>)[]): void + { + // Initialize a new array of updated promised rows. + const updatedPromiseRows: Promised>[] = []; + + 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): void + { + if (!("cells" in newRow) || !Object.values(newRow?.cells).some((cellData: Promisable) => ( + 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, + }; + } + + // 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][]).map(([columnKey, cellData]) => ( + // Return the same entry, with a promised instead of a promisable. + [columnKey, (this.promisedRows?.[rowId]?.cells?.[columnKey] ?? new Promised(this.handleNewCell.bind(this, rowId, columnKey))).refresh(cellData)] + )) + ) as Record>, + }); + + // 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, + 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 + { + 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; + } + } + + return newState; + } +} + +export class Promised +{ + promise?: Promise; + + data?: T; + + onData?: (data: T) => void; + + constructor(onChanged?: (data: T) => void, data?: Promisable) + { + this.onData = onChanged; + if (data) + this.refresh(data); + } + + refresh(data: Promisable): 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; + } +} diff --git a/src/Smartable/Column.tsx b/src/Smartable/Column.tsx index 3634b50..83aab33 100644 --- a/src/Smartable/Column.tsx +++ b/src/Smartable/Column.tsx @@ -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 ) } + +/** + * 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)); +} diff --git a/src/Smartable/Instance.tsx b/src/Smartable/Instance.tsx index 0a6dbb3..c00f383 100644 --- a/src/Smartable/Instance.tsx +++ b/src/Smartable/Instance.tsx @@ -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() // 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. + + )) + ); + + /*return ( >)[]> promise={data}> {(rowsData) => ( // Rendering defined rows. @@ -73,5 +88,9 @@ export function TableBody() )} - ); + );*/ +} + +export function TableRows() +{ } diff --git a/src/Utils.tsx b/src/Utils.tsx new file mode 100644 index 0000000..e69de29