Skip to content

Commit

Permalink
feat: allow column specific settings
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Sep 10, 2024
1 parent 900ecaf commit 5d975cb
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 56 deletions.
92 changes: 46 additions & 46 deletions src/table.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/* eslint-disable react/prop-types */

import cliTruncate from 'cli-truncate'
Expand All @@ -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.
Expand All @@ -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 =
Expand Down Expand Up @@ -81,64 +83,47 @@ export function Table<T extends ScalarDict>(props: TableProps<T>) {
columns: props.columns ?? allKeysInCollection(data),
data: processedData,
headerOptions,
horizontalAlignment,
maxWidth: determineConfiguredWidth(maxWidth),
overflow,
padding,
verticalAlignment,
}

const headings = getHeadings(config)
const columns = getColumns(config, headings)

const dataComponent = row<T>({
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<T>({
cell: Skeleton,
horizontalAlignment,
overflow,
padding,
skeleton: BORDER_SKELETONS[config.borderStyle].footer,
})

const headerComponent = row<T>({
cell: Skeleton,
horizontalAlignment,
overflow,
padding,
skeleton: BORDER_SKELETONS[config.borderStyle].header,
})

const {headerFooter} = BORDER_SKELETONS[config.borderStyle]
const headerFooterComponent = headerFooter ? row<T>({
cell: Skeleton,
horizontalAlignment,
overflow,
padding,
skeleton: headerFooter,
}) : () => false
const headerFooterComponent = headerFooter
? row<T>({
cell: Skeleton,
skeleton: headerFooter,
})
: () => false

const headingComponent = row<T>({
cell: Header,
horizontalAlignment,
overflow,
padding,
props: config.headerOptions,
skeleton: BORDER_SKELETONS[config.borderStyle].heading,
})

const separatorComponent = row<T>({
cell: Skeleton,
horizontalAlignment,
overflow,
padding,
skeleton: BORDER_SKELETONS[config.borderStyle].separator,
})

Expand All @@ -151,15 +136,26 @@ export function Table<T extends ScalarDict>(props: TableProps<T>) {
const maxKeyLength = Math.max(...Object.values(headings).map((c) => c.length))
// Construct a row.
return (
<Box key={key} borderTop borderBottom={false} borderLeft={false} borderRight={false} flexDirection="column" borderStyle="single">
<Box
key={key}
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
flexDirection="column"
borderStyle="single"
>
{/* 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 (
<Box key={`${key}-cell-${column.key}`} flexWrap='wrap'>
<Text {...config.headerOptions}>{keyName}{keyPadding}</Text>
<Box key={`${key}-cell-${column.key}`} flexWrap="wrap">
<Text {...config.headerOptions}>
{keyName}
{keyPadding}
</Text>
<Text wrap={overflow}>{value}</Text>
</Box>
)
Expand Down Expand Up @@ -198,18 +194,19 @@ export function Table<T extends ScalarDict>(props: TableProps<T>) {
*/
function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => 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) {
const key = `${props.key}-empty-${column.key}`

return (
<config.cell key={key} column={colI} {...config.props}>
{skeleton.line.repeat(column.width)}
{skeleton.line.repeat(width)}
</config.cell>
)
}
Expand All @@ -219,19 +216,20 @@ function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => 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
Expand All @@ -246,8 +244,9 @@ function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => R
marginLeft = spaces - marginRight
}

const alignItems = verticalAlignment === 'top' ? 'flex-start' : verticalAlignment === 'center' ? 'center' : 'flex-end'
return (
<config.cell key={key} column={colI} {...config.props}>
<config.cell key={key} column={colI} {...{alignItems}} {...config.props}>
{`${skeleton.line.repeat(marginLeft)}${v}${skeleton.line.repeat(marginRight)}`}
</config.cell>
)
Expand Down Expand Up @@ -290,7 +289,6 @@ export function Cell(props: CellProps) {
<Box {...props}>
<Text>{props.children}</Text>
</Box>

)
}

Expand Down Expand Up @@ -327,7 +325,7 @@ function Container(props: ContainerProps) {

export function makeTables<T extends ScalarDict[]>(
tables: {[P in keyof T]: TableProps<T[P]>},
options?: Omit<ContainerProps, 'children'>
options?: Omit<ContainerProps, 'children'>,
): void {
const leftMargin = options?.marginLeft ?? options?.margin ?? 0
const rightMargin = options?.marginRight ?? options?.margin ?? 0
Expand All @@ -336,13 +334,15 @@ export function makeTables<T extends ScalarDict[]>(
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(
<Container {...options}>
{processed.map((table) => <Table key={sha1(table)} {...table} />)}
</Container>
{processed.map((table) => (
<Table key={sha1(table)} {...table} />
))}
</Container>,
)
instance.unmount()
}
34 changes: 26 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
export type ColumnProps<T> = {
/**
* 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<T> = {[K in keyof T]: ColumnProps<K>}[keyof T]

Expand Down Expand Up @@ -182,17 +201,15 @@ export type Config<T> = {
overflow: Overflow
headerOptions: HeaderOptions
borderStyle: BorderStyle
horizontalAlignment: HorizontalAlignment
verticalAlignment: VerticalAlignment
}

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.
*/
Expand All @@ -208,10 +225,7 @@ export type RowConfig = {
cross: string
line: string
}
overflow?: Overflow
props?: Record<string, unknown>
horizontalAlignment?: HorizontalAlignment
verticalAlignment?: VerticalAlignment
}

export type RowProps<T extends ScalarDict> = {
Expand All @@ -224,6 +238,10 @@ export type Column<T> = {
key: string
column: keyof T
width: number
padding: number
horizontalAlignment: HorizontalAlignment
verticalAlignment: VerticalAlignment
overflow: Overflow
}

export type ContainerProps = {
Expand Down
9 changes: 7 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ export function allKeysInCollection<T extends ScalarDict>(data: T[]): (keyof T)[
}

export function getColumns<T extends ScalarDict>(config: Config<T>, headings: Partial<T>): Column<T>[] {
const {columns, maxWidth, padding} = config
const {columns, horizontalAlignment, maxWidth, overflow, verticalAlignment} = config

const widths: Column<T>[] = columns.map((propsOrKey) => {
const props: ColumnProps<keyof T> = 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) => {
Expand All @@ -65,7 +66,11 @@ export function getColumns<T extends ScalarDict>(config: Config<T>, headings: Pa

return {
column: key,
horizontalAlignment: props.horizontalAlignment ?? horizontalAlignment,
key: String(key),
overflow: props.overflow ?? overflow,
padding,
verticalAlignment: props.verticalAlignment ?? verticalAlignment,
width,
}
})
Expand All @@ -84,7 +89,7 @@ export function getColumns<T extends ScalarDict>(config: Config<T>, 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
Expand Down

0 comments on commit 5d975cb

Please sign in to comment.