Skip to content

Commit

Permalink
feat: filtering and sorting options
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 22, 2024
1 parent b69a75a commit 38efda2
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 9 deletions.
21 changes: 16 additions & 5 deletions examples/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const data = [
employed: ansis.bold('true'),
id: terminalLink('49032', 'https://example.com/bob'),
moreBigData: 'b'.repeat(30),
name: 'Bob',
name: ansis.dim('Bob'),
},
{
age: 22,
bigData: 'c'.repeat(30),
employed: ansis.bold('true'),
employed: ansis.bold('false'),
id: terminalLink('51786', 'https://example.com/charlie'),
moreBigData: 'c'.repeat(30),
name: 'Charlie',
Expand Down Expand Up @@ -873,13 +873,13 @@ const deployResult = [
{
filePath: 'force-app/main/default/classes/FileUtilities.cls',
fullName: 'FileUtilities',
state: 'Unchanged',
state: 'Changed',
type: 'ApexClass',
},
{
filePath: 'force-app/main/default/classes/FileUtilities.cls-meta.xml',
fullName: 'FileUtilities',
state: 'Unchanged',
state: 'Changed',
type: 'ApexClass',
},
{
Expand Down Expand Up @@ -1822,7 +1822,10 @@ const deployResult = [
// // 'evenMoreBigData',
// ],
// data,
// headerFormatter: 'capitalCase',
// filter: {
// employed: false,
// // name: /^B/,
// },
// headerOptions: {
// bold: true,
// color: '#905de8',
Expand Down Expand Up @@ -1856,10 +1859,18 @@ makeTable({
borderStyle: 'headers-only-with-underline',
columns: ['state', 'fullName', 'type'],
data: deployResult,
// filter: {
// state: 'Changed',
// type: /^Apex/,
// },
headerOptions: {
bold: true,
color: 'blueBright',
formatter: 'capitalCase',
},
overflow: 'wrap',
sort: {
fullName: 'asc',
type: 'asc',
},
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@types/react": "^18.3.3",
"change-case": "^5.4.4",
"ink": "^5.0.1",
"natural-orderby": "^3.0.2",
"object-hash": "^3.0.0",
"react": "^18.3.1",
"strip-ansi": "^7.1.0"
Expand Down
88 changes: 84 additions & 4 deletions src/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable react/prop-types */
import {camelCase, capitalCase, constantCase, kebabCase, pascalCase, sentenceCase, snakeCase} from 'change-case'
import {Box, Text, render} from 'ink'
import { orderBy } from 'natural-orderby';
import {sha1} from 'object-hash'
import React from 'react'
import stripAnsi from 'strip-ansi'
Expand All @@ -17,11 +18,11 @@ import {BORDER_SKELETONS, BorderStyle} from './skeletons.js'
* - [x] builtin column header formatters (e.g. capitalize, uppercase, etc.)
* - [x] make column headers customizable
* - [x] make borders removable
* - [ ] add tests (variable height, header formatter, header options, border styles)
* - [x] more border styles
* - [x] options for sorting, filtering
* - [ ] add tests (variable height, header formatter, header options, border styles, filtering, sorting)
*
* Features to consider:
* - [ ] more border styles
* - [ ] options for sorting, filtering, showing extended columns
* - [ ] alt text for truncated cells
* - [ ] side by side tables
*
Expand Down Expand Up @@ -135,6 +136,53 @@ export type TableProps<T extends ScalarDict> = {
* Align data in columns. Defaults to 'left'.
*/
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/}})
* ```
*/
filter?: Partial<Record<keyof T, boolean | string | RegExp>>
/**
* 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'.
*
* @example
* ```js
*
* const data = [
* {name: 'Alice', age: 30},
* {name: 'Bob', age: 25},
* {name: 'Charlie', age: 35},
* ]
*
* // sort the name column in ascending order
* makeTable({data, sort: {name: 'asc'}})
*
* // sort the name column in descending order
* makeTable({data, sort: {name: 'desc'}})
*
* // sort by name in ascending order and age in descending order
* makeTable({data, sort: {name: 'asc', age: 'desc'}})
* ```
*/
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
}

type Config<T> = {
Expand Down Expand Up @@ -358,6 +406,34 @@ function determineWidthToUse<T>(columns: Column<T>[], configuredWidth: number):
return tableWidth < configuredWidth ? configuredWidth : tableWidth
}

function filterData<T extends ScalarDict>(data: T[], filter: Partial<Record<keyof T, boolean | string | RegExp>>): 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
})
}

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

export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> & Partial<TableProps<T>>) {
const config: Config<T> = {
align: props.align ?? 'left',
Expand All @@ -376,12 +452,16 @@ export function Table<T extends ScalarDict>(props: Pick<TableProps<T>, 'data'> &
const headings = getHeadings(config)
const builder = new Builder<T>(config)


const data = sortData(filterData(props.data, props.filter ?? {}), props.sort)


return (
<Box flexDirection="column" width={determineWidthToUse(columns, config.maxWidth)}>
{builder.header({columns, data: {}, key: 'header'})}
{builder.heading({columns, data: headings, key: 'heading'})}
{builder.headerFooter({columns, data: {}, key: 'footer'})}
{props.data.map((row, index) => {
{data.map((row, index) => {
// Calculate the hash of the row based on its value and position
const key = `row-${sha1(row)}-${index}`

Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2895,6 +2895,11 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==

natural-orderby@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-3.0.2.tgz#1b874d685fbd68beab2c6e7d14f298e03d631ec3"
integrity sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g==

nise@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/nise/-/nise-6.0.0.tgz#ae56fccb5d912037363c3b3f29ebbfa28bde8b48"
Expand Down

0 comments on commit 38efda2

Please sign in to comment.