From 38efda2cc6b1ad3cb8eaed3ee47411f6c658042a Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 22 Aug 2024 13:51:38 -0600 Subject: [PATCH] feat: filtering and sorting options --- examples/basic.ts | 21 ++++++++--- package.json | 1 + src/table.tsx | 88 ++++++++++++++++++++++++++++++++++++++++++++--- yarn.lock | 5 +++ 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/examples/basic.ts b/examples/basic.ts index 219e8c4..531b789 100644 --- a/examples/basic.ts +++ b/examples/basic.ts @@ -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', @@ -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', }, { @@ -1822,7 +1822,10 @@ const deployResult = [ // // 'evenMoreBigData', // ], // data, -// headerFormatter: 'capitalCase', +// filter: { +// employed: false, +// // name: /^B/, +// }, // headerOptions: { // bold: true, // color: '#905de8', @@ -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', + }, }) diff --git a/package.json b/package.json index b98609c..ed9f04c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/table.tsx b/src/table.tsx index dc049b7..f43e227 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -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' @@ -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 * @@ -135,6 +136,53 @@ export type TableProps = { * 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> + /** + * 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> } type Config = { @@ -358,6 +406,34 @@ function determineWidthToUse(columns: Column[], configuredWidth: number): return tableWidth < configuredWidth ? configuredWidth : tableWidth } +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 + }) +} + +function sortData(data: T[], sort?: Partial> | 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(props: Pick, 'data'> & Partial>) { const config: Config = { align: props.align ?? 'left', @@ -376,12 +452,16 @@ export function Table(props: Pick, 'data'> & const headings = getHeadings(config) const builder = new Builder(config) + + const data = sortData(filterData(props.data, props.filter ?? {}), props.sort) + + return ( {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}` diff --git a/yarn.lock b/yarn.lock index 6406d04..321a878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"