Skip to content

Commit

Permalink
feat: clean up and sort with fn
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 27, 2024
1 parent bf20670 commit e758b0e
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 58 deletions.
33 changes: 22 additions & 11 deletions examples/basic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ansis from 'ansis'
import {pathToFileURL} from 'node:url'
import terminalLink from 'terminal-link'

import {makeTable} from '../src/index.js'
Expand Down Expand Up @@ -870,9 +869,10 @@ const versions = [
},
].map((v) => ({
...v,
location: v.location.startsWith('http')
? terminalLink(new URL(v.location).pathname.split('/').at(-1) ?? v.location, v.location)
: v.location,
// location: v.location.startsWith('http')
// ? terminalLink(new URL(v.location).pathname.split('/').at(-1) ?? v.location, v.location)
// : v.location,
num: 1,
}))

const deployResult = [
Expand Down Expand Up @@ -1844,7 +1844,7 @@ const deployResult = [

makeTable({
borderStyle: 'headers-only-with-underline',
columns: ['version', 'location', 'channel'],
columns: ['version', 'channel', 'location'],
data: versions,
// filter: (row) => /^.+$/.test(row.channel),
headerOptions: {
Expand All @@ -1853,15 +1853,26 @@ makeTable({
formatter: 'capitalCase',
},
overflow: 'wrap',
// sort: {
// channel: 'desc',
// },
sort: {
channel(a, b) {
// sort in this order: latest, latest-rc, nightly, qa, undefined
if (!a) return 1
if (!b) return -1

const order = ['latest', 'latest-rc', 'nightly', 'qa']
return order.indexOf(a) - order.indexOf(b)
},
num(a, b) {
return a - b
},
},
})
// console.log()
console.log()
makeTable({
align: 'left',
// align: 'center',
borderStyle: 'headers-only-with-underline',
columns: ['state', 'fullName', 'type'],
// borderStyle: 'all',
columns: ['state', 'fullName', 'type', {key: 'filePath', name: 'Path'}],
data: deployResult,
filter: (row) => row.state === 'Changed' && row.type.startsWith('A'),
headerOptions: {
Expand Down
36 changes: 36 additions & 0 deletions examples/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ansis from 'ansis'

import {makeTable} from '../src/index.js'
import {BORDER_STYLES} from '../src/skeletons.js'

const data = [
{
age: 20,
id: '36329',
name: 'Alice',
},
{
age: 21,
id: '49032',
name: 'Bob',
},
{
age: 22,
id: '51786',
name: 'Charlie',
},
]

for (const borderStyle of BORDER_STYLES) {
console.log(ansis.bold(borderStyle))
makeTable({
align: 'center',
borderStyle,
columns: ['id', {key: 'name', name: 'First Name'}, 'age'],
data,
headerOptions: {
formatter: 'capitalCase',
},
})
console.log()
}
25 changes: 14 additions & 11 deletions src/skeletons.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
export type BorderStyle =
| 'all'
| 'headers-only-with-outline'
| 'headers-only-with-underline'
| 'headers-only'
| 'horizontal-with-outline'
| 'horizontal'
| 'none'
| 'outline'
| 'vertical-with-outline'
| 'vertical'
export const BORDER_STYLES = [
'all',
'headers-only-with-outline',
'headers-only-with-underline',
'headers-only',
'horizontal-with-outline',
'horizontal',
'none',
'outline',
'vertical-with-outline',
'vertical',
] as const

export type BorderStyle = (typeof BORDER_STYLES)[number]

type Skeleton = {
cross: string
Expand Down
69 changes: 48 additions & 21 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,29 @@ import React from 'react'
import stripAnsi from 'strip-ansi'

import {BORDER_SKELETONS} from './skeletons.js'
import {CellProps, Column, Config, Percentage, RowConfig, RowProps, ScalarDict, TableProps} from './types.js';
import {
CellProps,
Column,
Config,
HeaderOptions,
Percentage,
RowConfig,
RowProps,
ScalarDict,
TableProps
} from './types.js';
import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData, truncate, wrap} from './utils.js';

/**
* Determines the configured width based on the provided width value.
* If no width is provided, it returns the width of the current terminal.
* If the provided width is a percentage, it calculates the width based on the percentage of the terminal width.
* If the provided width is a number, it returns the provided width.
* If the calculated width is greater than the terminal width, it returns the terminal width.
*
* @param providedWidth - The width value provided.
* @returns The determined configured width.
*/
function determineConfiguredWidth(providedWidth: number | Percentage | undefined): number {
if (!providedWidth) return process.stdout.columns

Expand Down Expand Up @@ -37,74 +57,80 @@ function determineWidthToUse<T>(columns: Column<T>[], configuredWidth: number):
return tableWidth < configuredWidth ? configuredWidth : tableWidth
}

export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> & Partial<TableProps<T>>) {
export function Table<T extends ScalarDict>(props: TableProps<T>) {
const {
align = 'left',
borderStyle = 'all',
data,
filter,
headerOptions = {bold: true, color: 'blue'},
maxWidth,
overflow = 'truncate',
padding = 1,
sort,
} = props

const headerOptions = {bold: true, color: 'blue', ...props.headerOptions} satisfies HeaderOptions
const processedData = sortData(filter ? data.filter((row) => filter(row)) : data, sort)
const config: Config<T> = {
align,
borderStyle,
columns: props.columns ?? allKeysInCollection(data),
data,
data: processedData,
headerOptions,
maxWidth: determineConfiguredWidth(maxWidth),
overflow,
padding,
}

const columns = getColumns(config)
const headings = getHeadings(config)
const processedData = sortData(filter ? data.filter((row) => filter(row)) : data, sort)

const dataComponent = row<T>({
align,
cell: Cell,
overflow: config.overflow,
padding: config.padding,
overflow,
padding,
skeleton: BORDER_SKELETONS[config.borderStyle].data,
})

const footerComponent = row<T>({
align,
cell: Skeleton,
overflow: config.overflow,
padding: config.padding,
overflow,
padding,
skeleton: BORDER_SKELETONS[config.borderStyle].footer,
})

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

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

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

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

Expand Down Expand Up @@ -135,7 +161,7 @@ export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> &
*/
function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => React.ReactNode {
// This is a component builder. We return a function.
const {overflow, padding, skeleton} = config
const {align, overflow, padding, skeleton} = config

return (props) => {
const data = props.columns.map((column, colI) => {
Expand Down Expand Up @@ -164,12 +190,13 @@ function row<T extends ScalarDict>(config: RowConfig): (props: RowProps<T>) => R
overflow === 'wrap'
? column.width - stripAnsi(v).split('\n')[0].trim().length
: column.width - stripAnsi(v).length

let marginLeft: number
let marginRight: number
if (column.align === 'left') {
if (align === 'left') {
marginLeft = padding
marginRight = spaces - marginLeft
} else if (column.align === 'center') {
} else if (align === 'center') {
marginLeft = Math.floor(spaces / 2)
marginRight = Math.ceil(spaces / 2)
} else {
Expand Down
24 changes: 17 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export type HeaderOptions = {
formatter?: HeaderFormatter
}

type Overflow = 'wrap' | 'truncate'

type SortOrder<T> = 'asc' | 'desc' | ((valueA: T, valueB: T) => number)

export type Sort<T> = {
[K in keyof T]?: SortOrder<T[K]>
}

export type TableProps<T extends ScalarDict> = {
/**
* List of values (rows).
Expand All @@ -94,7 +102,7 @@ export type TableProps<T extends ScalarDict> = {
/**
* Overflow behavior for cells. Defaults to 'truncate'.
*/
overflow?: 'wrap' | 'truncate'
overflow?: Overflow
/**
* Styling options for the column headers
*/
Expand All @@ -114,7 +122,7 @@ export type TableProps<T extends ScalarDict> = {
/**
* Sort the data in the table.
*
* Each key in the object should correspond to a column in the table. The value can be 'asc' or 'desc'.
* Each key in the object should correspond to a column in the table. The value can be 'asc', 'desc', or a custom sort function.
*
* The order of the keys determines the order of the sorting. The first key is the primary sort key, the second key is the secondary sort key, and so on.
*
Expand All @@ -134,18 +142,20 @@ export type TableProps<T extends ScalarDict> = {
*
* // sort by name in ascending order and age in descending order
* makeTable({data, sort: {name: 'asc', age: 'desc'}})
*
* // sort by name in ascending order and age in descending order using a custom sort function
* makeTable({data, sort: {name: 'asc', age: (a, b) => b - a}})
* ```
*/
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
sort?: Sort<T>
}

export type Config<T> = {
align: ColumnAlignment
columns: (keyof T | AllColumnProps<T>)[]
data: T[]
padding: number
maxWidth: number
overflow: 'wrap' | 'truncate'
overflow: Overflow
headerOptions: HeaderOptions
borderStyle: BorderStyle
}
Expand Down Expand Up @@ -174,8 +184,9 @@ export type RowConfig = {
cross: string
line: string
}
overflow?: 'wrap' | 'truncate'
overflow?: Overflow
props?: Record<string, unknown>
align?: ColumnAlignment
}

export type RowProps<T extends ScalarDict> = {
Expand All @@ -188,5 +199,4 @@ export type Column<T> = {
key: string
column: keyof T
width: number
align: ColumnAlignment
}
12 changes: 4 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {camelCase, capitalCase, constantCase, kebabCase, pascalCase, sentenceCas
import {orderBy} from 'natural-orderby'
import stripAnsi from 'strip-ansi'

import {Column, ColumnProps, Config, ScalarDict} from './types.js'
import {Column, ColumnProps, Config, ScalarDict, Sort} from './types.js'

/**
* Intersperses a list of elements with another element.
Expand All @@ -27,13 +27,10 @@ export function intersperse<T, I>(intersperser: (index: number) => I, elements:
return interspersed
}

export function sortData<T extends ScalarDict>(
data: T[],
sort?: Partial<Record<keyof T, 'asc' | 'desc'>> | undefined,
): T[] {
export function sortData<T extends ScalarDict>(data: T[], sort?: Sort<T> | undefined): T[] {
if (!sort) return data
const identifiers = Object.keys(sort)
const orders = identifiers.map((i) => sort[i]) as ['asc' | 'desc']
const orders = Object.values(sort)
return orderBy(data, identifiers, orders)
}

Expand Down Expand Up @@ -72,7 +69,7 @@ export function allKeysInCollection<T extends ScalarDict>(data: T[]): (keyof T)[
}

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

const widths: Column<T>[] = columns.map((propsOrKey) => {
const props: ColumnProps<keyof T> = typeof propsOrKey === 'object' ? propsOrKey : {key: propsOrKey}
Expand All @@ -90,7 +87,6 @@ export function getColumns<T extends ScalarDict>(config: Config<T>): Column<T>[]
const width = Math.max(...data, header) + padding * 2

return {
align: align ?? 'left',
column: key,
key: String(key),
width,
Expand Down

0 comments on commit e758b0e

Please sign in to comment.