process.env.NODE_ENV = "production"; import preact from "preact"; import { css } from "typesafecss"; import { formatValue, JSXFormatter, toSpaceCase } from "./GenericFormat"; import { observer } from "./observer"; import { canHaveChildren } from "socket-function/src/types"; import { showFullscreenModal } from "./FullscreenModal"; import { observable } from "mobx"; // Null means the column is removed export type ColumnType = undefined | null | { center?: boolean; // Defaults to column name title?: preact.ComponentChild; formatter?: JSXFormatter; }; export type RowType = { [columnName: string]: unknown; }; export type ColumnsType = { [columnName: string]: ColumnType }; export type TableType = { columns: { [columnName in keyof RowT]?: ColumnType }; rows: RowT[]; }; @observer export class Table extends preact.Component & { class?: string; cellClass?: string; initialLimit?: number; // Line and character limits before we cut off the inner content lineLimit?: number; characterLimit?: number; excludeEmptyColumns?: boolean; getRowFields?: (row: RowT) => preact.JSX.HTMLAttributes; }> { state = observable({ limit: this.props.initialLimit || 100, }); render() { let { columns, rows, excludeEmptyColumns } = this.props; let cellClass = " " + String(this.props.cellClass || "") + " "; let allRows = rows; rows = rows.slice(0, this.state.limit); const lineLimit = this.props.lineLimit ?? 3; const characterLimit = this.props.characterLimit ?? 300; if (excludeEmptyColumns) { columns = { ...columns }; for (let column of Object.keys(columns)) { if (!rows.some(row => row[column] !== undefined && row[column] !== null)) { delete columns[column]; } } } return ( {Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]: [string, ColumnType]) => )} {rows.map((row, index) => { let rowFields = this.props.getRowFields?.(row) || {}; return {Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]: [string, ColumnType]) => { let value = row[columnName]; let formatter = column?.formatter || "guess"; let result = formatValue(value, formatter, { row, columnName }); let renderedObj = renderTrimmed({ content: result, lineLimit, characterLimit, }); let attributes = { ...renderedObj.outerAttributes }; attributes.class = attributes.class || ""; attributes.class += " " + css.whiteSpace("pre-wrap").pad2(8, 4); if (column?.center) attributes.class += " " + css.verticalAlign("middle").textAlign("center"); attributes.class += cellClass; // If the inner content looks like a VNode, take it's attributes and unwrap it, // so it can fill the entire cell. let innerContent = renderedObj.innerContent; if ( canHaveChildren(innerContent) && "props" in innerContent && canHaveChildren(innerContent.props) && "children" in innerContent.props && ( Array.isArray(innerContent.props.children) && innerContent.props.children.length === 1 || !Array.isArray(innerContent.props.children) ) // AND, it is a div or span (a tags shouldn't be unwrapped) && (innerContent.type === "div") ) { attributes.class += " " + innerContent.props.class; let baseOnClick = attributes.onClick; let props = innerContent.props; attributes.onClick = (e) => { if (baseOnClick) baseOnClick(e); (props as any).onClick?.(e); }; for (let key in props) { if (key === "class") continue; if (key === "onClick") continue; (attributes as any)[key] = props[key]; } innerContent = props.children as any; } return ; })} ; })} {allRows.length > rows.length && }
⧉ {allRows.length}{column?.title || toSpaceCase(columnName)}
{index + 1} {innerContent}
); } } function renderTrimmed(config: { content: preact.ComponentChild; lineLimit: number; characterLimit: number; }): { outerAttributes: preact.JSX.HTMLAttributes; innerContent: preact.ComponentChild; } { let { content, lineLimit, characterLimit } = config; if (typeof content !== "string" && typeof content !== "number") { return { outerAttributes: {}, innerContent: content as any, }; } let trimmed = false; let contentStr = String(content); if (contentStr.length > characterLimit) { contentStr = contentStr.slice(0, characterLimit - 3) + "..."; trimmed = true; } let lines = contentStr.split("\n"); if (lines.length > lineLimit) { lines = lines.slice(0, lineLimit); lines[lines.length - 1] += "..."; contentStr = lines.join("\n"); trimmed = true; } if (!trimmed) { return { outerAttributes: {}, innerContent: contentStr, }; } return { outerAttributes: { class: css.opacity(0.5, "hover").button, onClick: () => { showFullscreenModal( { contents:
{content}
}, ); } }, innerContent: contentStr }; }