diff --git a/examples/basic.ts b/examples/basic.ts index 42d0903..5694008 100644 --- a/examples/basic.ts +++ b/examples/basic.ts @@ -1,4 +1,5 @@ import ansis from 'ansis' +import {pathToFileURL} from 'node:url' import terminalLink from 'terminal-link' import {makeTable} from '../src/index.js' @@ -867,7 +868,12 @@ const versions = [ 'https://developer.salesforce.com/media/salesforce-cli/sf/versions/2.38.6/1d0ec8e/sf-v2.38.6-1d0ec8e-darwin-arm64.tar.gz', version: '2.38.6', }, -] +].map((v) => ({ + ...v, + location: v.location.startsWith('http') + ? terminalLink(new URL(v.location).pathname.split('/').at(-1) ?? v.location, v.location) + : v.location, +})) const deployResult = [ { @@ -1840,27 +1846,24 @@ makeTable({ borderStyle: 'headers-only-with-underline', columns: ['version', 'location', 'channel'], data: versions, - filter: { - channel: /^.+$/, - // version: /^2.5\d/, - }, + // filter: (row) => /^.+$/.test(row.channel), headerOptions: { bold: true, color: '#905de8', formatter: 'capitalCase', }, overflow: 'wrap', + // sort: { + // channel: 'desc', + // }, }) - +// console.log() makeTable({ align: 'left', borderStyle: 'headers-only-with-underline', columns: ['state', 'fullName', 'type'], data: deployResult, - filter: { - state: 'Changed', - type: /^A/, - }, + filter: (row) => row.state === 'Changed' && row.type.startsWith('A'), headerOptions: { bold: true, color: 'blueBright', diff --git a/src/table.tsx b/src/table.tsx index dd0587b..787a52e 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -8,7 +8,7 @@ import stripAnsi from 'strip-ansi' import {BORDER_SKELETONS} from './skeletons.js' import {CellProps, Column, Config, Percentage, RowConfig, RowProps, ScalarDict, TableProps} from './types.js'; -import {allKeysInCollection, filterData, getColumns, getHeadings, intersperse, sortData, truncate, wrap} from './utils.js'; +import {allKeysInCollection, getColumns, getHeadings, intersperse, sortData, truncate, wrap} from './utils.js'; function determineConfiguredWidth(providedWidth: number | Percentage | undefined): number { if (!providedWidth) return process.stdout.columns @@ -38,22 +38,31 @@ function determineWidthToUse(columns: Column[], configuredWidth: number): } export function Table(props: Pick, 'data'> & Partial>) { + const { + align = 'left', + borderStyle = 'all', + data, + filter, + headerOptions = {bold: true, color: 'blue'}, + maxWidth, + overflow = 'truncate', + padding = 1, + sort, + } = props + const config: Config = { - align: props.align ?? 'left', - borderStyle: props.borderStyle ?? 'all', - columns: props.columns ?? allKeysInCollection(props.data), - data: props.data, - headerOptions: props.headerOptions ?? { - bold: true, - color: 'blue', - }, - maxWidth: determineConfiguredWidth(props.maxWidth), - overflow: props.overflow ?? 'truncate', - padding: props.padding ?? 1, + align, + borderStyle, + columns: props.columns ?? allKeysInCollection(data), + data, + headerOptions, + maxWidth: determineConfiguredWidth(maxWidth), + overflow, + padding, } const columns = getColumns(config) const headings = getHeadings(config) - const data = sortData(filterData(props.data, props.filter ?? {}), props.sort) + const processedData = sortData(filter ? data.filter((row) => filter(row)) : data, sort) const dataComponent = row({ cell: Cell, @@ -100,11 +109,11 @@ export function Table(props: Pick, 'data'> & }) return ( - + {headerComponent({columns, data: {}, key: 'header'})} {headingComponent({columns, data: headings, key: 'heading'})} {headerFooterComponent({columns, data: {}, key: 'footer'})} - {data.map((row, index) => { + {processedData.map((row, index) => { // Calculate the hash of the row based on its value and position const key = `row-${sha1(row)}-${index}` @@ -188,11 +197,8 @@ function row(config: RowConfig): (props: RowProps) => R return ( - {/* Left */} {skeleton.left} - {/* Data */} {...elements} - {/* Right */} {skeleton.right} ) diff --git a/src/types.ts b/src/types.ts index 8c070ff..5ba5ade 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,31 +108,16 @@ export type TableProps = { */ align?: ColumnAlignment /** - * Filter the data in the table. - * - * Each key in the object should correspond to a column in the table. The value can be a string or a regular expression. - * - * @example - * ```js - * const data = [ - * {name: 'Alice', age: 30}, - * {name: 'Bob', age: 25}, - * {name: 'Charlie', age: 35}, - * ] - * - * // filter the name column with a string - * makeTable({data, filter: {name: 'Alice'}}) - * - * // filter the name column with a regular expression - * makeTable({data, filter: {name: /^A/}}) - * ``` + * Apply a filter to each row in the table. */ - filter?: Partial> + filter?: (row: T) => boolean /** * 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'. * + * 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. + * * @example * ```js * const data = [ diff --git a/src/utils.ts b/src/utils.ts index bd598d0..cfca8bb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,30 +27,6 @@ export function intersperse(intersperser: (index: number) => I, elements: return interspersed } -export function filterData( - data: T[], - filter: Partial>, -): T[] { - return data.filter((row) => { - for (const key in row) { - if (key in row) { - const f = filter[key] - const value = stripAnsi(String(row[key])) - if (f !== undefined && typeof f === 'boolean') { - const convertedBoolean = value === 'true' ? true : value === 'false' ? false : value - return f === convertedBoolean - } - - if (!f) continue - if (typeof f === 'string' && !value.includes(f)) return false - if (f instanceof RegExp && !f.test(value)) return false - } - } - - return true - }) -} - export function sortData( data: T[], sort?: Partial> | undefined, diff --git a/test/util.test.ts b/test/util.test.ts index 57ff814..f6e206e 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -1,6 +1,6 @@ import {config, expect} from 'chai' -import {filterData, intersperse, sortData, truncate, wrap} from '../src/utils.js' +import {intersperse, sortData, truncate, wrap} from '../src/utils.js' config.truncateThreshold = 0 @@ -12,48 +12,6 @@ describe('intersperse', () => { }) }) -describe('filterData', () => { - it('should filter data using string', () => { - const data = [ - {age: 30, name: 'Alice'}, - {age: 25, name: 'Bob'}, - {age: 35, name: 'Charlie'}, - ] - const filter = {name: 'Alice'} - const expected = [{age: 30, name: 'Alice'}] - expect(filterData(data, filter)).to.deep.equal(expected) - }) - - it('should filter data using regular expression', () => { - const data = [ - {age: 30, name: 'Alice'}, - {age: 25, name: 'Bob'}, - {age: 35, name: 'Charlie'}, - ] - const filter = {name: /^A/} - const expected = [{age: 30, name: 'Alice'}] - expect(filterData(data, filter)).to.deep.equal(expected) - }) - - it('should filter data using boolean', () => { - const data = [ - {age: 30, employed: true, name: 'Alice'}, - {age: 25, employed: false, name: 'Bob'}, - {age: 35, employed: false, name: 'Charlie'}, - ] - const filter = {employed: true} - const expected = [{age: 30, employed: true, name: 'Alice'}] - expect(filterData(data, filter)).to.deep.equal(expected) - }) - - it('should filter despite ansi characters', () => { - const data = [{name: '\u001B[31mAlice\u001B[39m'}] - const filter = {name: 'Alice'} - const expected = data - expect(filterData(data, filter)).to.deep.equal(expected) - }) -}) - describe('sortData', () => { it('should sort data in ascending order', () => { const data = [