diff --git a/src/table.tsx b/src/table.tsx index 791252e..44651fa 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -1,4 +1,3 @@ - /* eslint-disable react/prop-types */ import cliTruncate from 'cli-truncate' @@ -19,9 +18,9 @@ import { RowConfig, RowProps, ScalarDict, - TableProps -} from './types.js'; -import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData} from './utils.js'; + TableProps, +} from './types.js' +import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData} from './utils.js' /** * Determines the configured width based on the provided width value. @@ -33,7 +32,10 @@ import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData} fro * @param providedWidth - The width value provided. * @returns The determined configured width. */ -function determineConfiguredWidth(providedWidth: number | Percentage | undefined, columns = process.stdout.columns): number { +function determineConfiguredWidth( + providedWidth: number | Percentage | undefined, + columns = process.stdout.columns, +): number { if (!providedWidth) return columns const num = @@ -81,9 +83,11 @@ export function Table(props: TableProps) { columns: props.columns ?? allKeysInCollection(data), data: processedData, headerOptions, + horizontalAlignment, maxWidth: determineConfiguredWidth(maxWidth), overflow, padding, + verticalAlignment, } const headings = getHeadings(config) @@ -91,54 +95,35 @@ export function Table(props: TableProps) { const dataComponent = row({ cell: Cell, - horizontalAlignment, - overflow, - padding, - props: { - alignItems: verticalAlignment === 'top' ? 'flex-start' : verticalAlignment === 'center' ? 'center' : 'flex-end', - }, skeleton: BORDER_SKELETONS[config.borderStyle].data, }) const footerComponent = row({ cell: Skeleton, - horizontalAlignment, - overflow, - padding, skeleton: BORDER_SKELETONS[config.borderStyle].footer, }) const headerComponent = row({ cell: Skeleton, - horizontalAlignment, - overflow, - padding, skeleton: BORDER_SKELETONS[config.borderStyle].header, }) const {headerFooter} = BORDER_SKELETONS[config.borderStyle] - const headerFooterComponent = headerFooter ? row({ - cell: Skeleton, - horizontalAlignment, - overflow, - padding, - skeleton: headerFooter, - }) : () => false + const headerFooterComponent = headerFooter + ? row({ + cell: Skeleton, + skeleton: headerFooter, + }) + : () => false const headingComponent = row({ cell: Header, - horizontalAlignment, - overflow, - padding, props: config.headerOptions, skeleton: BORDER_SKELETONS[config.borderStyle].heading, }) const separatorComponent = row({ cell: Skeleton, - horizontalAlignment, - overflow, - padding, skeleton: BORDER_SKELETONS[config.borderStyle].separator, }) @@ -151,15 +136,26 @@ export function Table(props: TableProps) { const maxKeyLength = Math.max(...Object.values(headings).map((c) => c.length)) // Construct a row. return ( - + {/* print all data in key:value pairs */} {columns.map((column) => { const value = (row[column.column] ?? '').toString() const keyName = (headings[column.key] ?? column.key).toString() const keyPadding = ' '.repeat(maxKeyLength - keyName.length + padding) return ( - - {keyName}{keyPadding} + + + {keyName} + {keyPadding} + {value} ) @@ -198,10 +194,11 @@ export function Table(props: TableProps) { */ function row(config: RowConfig): (props: RowProps) => React.ReactNode { // This is a component builder. We return a function. - const {horizontalAlignment, overflow, padding, skeleton} = config + const {skeleton} = config return (props) => { const data = props.columns.map((column, colI) => { + const {horizontalAlignment, overflow, padding, verticalAlignment, width} = column const value = props.data[column.column] if (value === undefined || value === null) { @@ -209,7 +206,7 @@ function row(config: RowConfig): (props: RowProps) => R return ( - {skeleton.line.repeat(column.width)} + {skeleton.line.repeat(width)} ) } @@ -219,19 +216,20 @@ function row(config: RowConfig): (props: RowProps) => R // https://github.com/sindresorhus/terminal-link/issues/18 // https://github.com/Shopify/cli/pull/995 const valueWithNoZeroWidthChars = String(value).replaceAll('​', ' ') - const spaceForText = column.width - (padding * 2) + const spaceForText = width - padding * 2 const v = // if the visible length of the value is greater than the column width, truncate or wrap stripAnsi(valueWithNoZeroWidthChars).length >= spaceForText ? overflow === 'wrap' - ? wrapAnsi(valueWithNoZeroWidthChars, spaceForText, {hard: true, trim: true}).replaceAll('\n', `${' '.repeat(padding)}\n${' '.repeat(padding)}`) + ? wrapAnsi(valueWithNoZeroWidthChars, spaceForText, {hard: true, trim: true}).replaceAll( + '\n', + `${' '.repeat(padding)}\n${' '.repeat(padding)}`, + ) : cliTruncate(valueWithNoZeroWidthChars, spaceForText) : valueWithNoZeroWidthChars const spaces = - overflow === 'wrap' - ? column.width - stripAnsi(v).split('\n')[0].trim().length - : column.width - stripAnsi(v).length + overflow === 'wrap' ? width - stripAnsi(v).split('\n')[0].trim().length : width - stripAnsi(v).length let marginLeft: number let marginRight: number @@ -246,8 +244,9 @@ function row(config: RowConfig): (props: RowProps) => R marginLeft = spaces - marginRight } + const alignItems = verticalAlignment === 'top' ? 'flex-start' : verticalAlignment === 'center' ? 'center' : 'flex-end' return ( - + {`${skeleton.line.repeat(marginLeft)}${v}${skeleton.line.repeat(marginRight)}`} ) @@ -290,7 +289,6 @@ export function Cell(props: CellProps) { {props.children} - ) } @@ -327,7 +325,7 @@ function Container(props: ContainerProps) { export function makeTables( tables: {[P in keyof T]: TableProps}, - options?: Omit + options?: Omit, ): void { const leftMargin = options?.marginLeft ?? options?.margin ?? 0 const rightMargin = options?.marginRight ?? options?.margin ?? 0 @@ -336,13 +334,15 @@ export function makeTables( const processed = tables.map((table) => ({ ...table, // adjust maxWidth to account for margin - maxWidth: determineConfiguredWidth(table.maxWidth, columns) + maxWidth: determineConfiguredWidth(table.maxWidth, columns), })) const instance = render( - {processed.map((table) => )} - + {processed.map((table) => ( +
+ ))} + , ) instance.unmount() } diff --git a/src/types.ts b/src/types.ts index c51654b..fd7523b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,9 +11,28 @@ export type CellProps = React.PropsWithChildren<{readonly column: number}> export type HorizontalAlignment = 'left' | 'right' | 'center' export type VerticalAlignment = 'top' | 'center' | 'bottom' -export interface ColumnProps { +export type ColumnProps = { + /** + * Horizontal alignment of cell content. Overrides the horizontal alignment set in the table. + */ + horizontalAlignment?: HorizontalAlignment key: T + /** + * Name of the column. If not provided, it will default to the key. + */ name?: string + /** + * Overflow behavior for cells. Overrides the overflow set in the table. + */ + overflow?: Overflow + /** + * Padding for the column. Overrides the padding set in the table. + */ + padding?: number + /** + * Vertical alignment of cell content. Overrides the vertical alignment set in the table. + */ + verticalAlignment?: VerticalAlignment } export type AllColumnProps = {[K in keyof T]: ColumnProps}[keyof T] @@ -182,6 +201,8 @@ export type Config = { overflow: Overflow headerOptions: HeaderOptions borderStyle: BorderStyle + horizontalAlignment: HorizontalAlignment + verticalAlignment: VerticalAlignment } export type RowConfig = { @@ -189,10 +210,6 @@ export type RowConfig = { * Component used to render cells. */ cell: (props: CellProps) => React.ReactNode - /** - * Tells the padding of each cell. - */ - padding: number /** * Component used to render skeleton in the row. */ @@ -208,10 +225,7 @@ export type RowConfig = { cross: string line: string } - overflow?: Overflow props?: Record - horizontalAlignment?: HorizontalAlignment - verticalAlignment?: VerticalAlignment } export type RowProps = { @@ -224,6 +238,10 @@ export type Column = { key: string column: keyof T width: number + padding: number + horizontalAlignment: HorizontalAlignment + verticalAlignment: VerticalAlignment + overflow: Overflow } export type ContainerProps = { diff --git a/src/utils.ts b/src/utils.ts index 37c026b..13ffaec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,11 +46,12 @@ export function allKeysInCollection(data: T[]): (keyof T)[ } export function getColumns(config: Config, headings: Partial): Column[] { - const {columns, maxWidth, padding} = config + const {columns, horizontalAlignment, maxWidth, overflow, verticalAlignment} = config const widths: Column[] = columns.map((propsOrKey) => { const props: ColumnProps = typeof propsOrKey === 'object' ? propsOrKey : {key: propsOrKey} const {key} = props + const padding = props.padding ?? config.padding // Get the width of each cell in the column const data = config.data.map((data) => { @@ -65,7 +66,11 @@ export function getColumns(config: Config, headings: Pa return { column: key, + horizontalAlignment: props.horizontalAlignment ?? horizontalAlignment, key: String(key), + overflow: props.overflow ?? overflow, + padding, + verticalAlignment: props.verticalAlignment ?? verticalAlignment, width, } }) @@ -84,7 +89,7 @@ export function getColumns(config: Config, headings: Pa const largestColumn = widths.reduce((a, b) => (a.width > b.width ? a : b)) const header = String(headings[largestColumn.key]).length // The minimum width of a column is the width of the header plus padding on both sides - const minWidth = header + padding * 2 + const minWidth = header + largestColumn.padding * 2 const difference = tableWidth - maxWidth const newWidth = largestColumn.width - difference < minWidth ? minWidth : largestColumn.width - difference largestColumn.width = newWidth