diff --git a/CONRTIBUTING.md b/CONRTIBUTING.md index cce18a7..8927b14 100644 --- a/CONRTIBUTING.md +++ b/CONRTIBUTING.md @@ -2,20 +2,20 @@ ## Reporting Bugs -When submitting a new bug report, please first [search](https://github.com/oclif/multi-stage-output/issues) for an existing or similar report & then use one of our existing [issue templates](https://github.com/oclif/multi-stage-output/issues/new/choose) if you believe you've come across a unique problem. Duplicate issues, or issues that don't use one of our templates may get closed without a response. +When submitting a new bug report, please first [search](https://github.com/oclif/table/issues) for an existing or similar report & then use one of our existing [issue templates](https://github.com/oclif/table/issues/new/choose) if you believe you've come across a unique problem. Duplicate issues, or issues that don't use one of our templates may get closed without a response. ## Development **1. Clone this repository...** ```bash -$ git clone git@github.com:oclif/multi-stage-output.git +$ git clone git@github.com:oclif/table.git ``` **2. Navigate into project & install development-specific dependencies...** ```bash -$ cd ./multi-stage-output && yarn +$ cd ./table && yarn ``` **3. Write some code &/or add some tests...** @@ -30,7 +30,7 @@ $ cd ./multi-stage-output && yarn $ yarn test ``` -**5. Open a [Pull Request](https://github.com/oclif/multi-stage-output/pulls) for your work & become the newest contributor to `@oclif/multi-stage-output`! 🎉** +**5. Open a [Pull Request](https://github.com/oclif/table/pulls) for your work & become the newest contributor to `@oclif/table`! 🎉** ## Pull Request Conventions diff --git a/examples/multiple.ts b/examples/multiple.ts index df68c47..eddf46a 100644 --- a/examples/multiple.ts +++ b/examples/multiple.ts @@ -63,6 +63,9 @@ const projects = [ }, ] +// Occasionally, if you have two maxWidths that add up to 100% they will stack vertically instead of horizontally. +// At first I thought this might be when the window has an odd number of pixels, but it seems to be more random than that. + const employeesTable: TableOptions<(typeof employees)[number]> = { columns: ['id', 'name', 'age', 'description'], data: employees, diff --git a/examples/orientation.ts b/examples/orientation.ts deleted file mode 100644 index 4926435..0000000 --- a/examples/orientation.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {printTable} from '../src/index.js' - -const description = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' - -const data = [ - { - description, - item: 'Item 1', - url: 'https://www.example.com/item/1', - }, - { - description, - item: 'Item 2', - url: 'https://www.example.com/item/2', - }, - { - description, - item: 'Item 3', - url: 'https://www.example.com/item/3', - }, -] - -printTable({ - columns: ['item', 'description', 'url'], - data, - headerOptions: { - formatter: 'capitalCase', - }, - horizontalAlignment: 'center', - overflow: 'wrap', - title: 'Horizontal Orientation', - titleOptions: {bold: true}, -}) - -printTable({ - columns: ['item', 'description', 'url'], - data, - headerOptions: { - formatter: 'capitalCase', - }, - horizontalAlignment: 'center', - orientation: 'vertical', - overflow: 'wrap', - title: 'Vertical Orientation', - titleOptions: {bold: true}, -}) diff --git a/examples/overflow.ts b/examples/overflow.ts index e5253e5..7aa33b2 100644 --- a/examples/overflow.ts +++ b/examples/overflow.ts @@ -82,6 +82,7 @@ printTable({ overflow: 'wrap', title: 'Wrap (aligned left)', titleOptions: {bold: true}, + verticalAlignment: 'center', }) printTable({ @@ -94,4 +95,5 @@ printTable({ overflow: 'wrap', title: 'Wrap (aligned right)', titleOptions: {bold: true}, + verticalAlignment: 'bottom', }) diff --git a/examples/sort-and-filter.ts b/examples/sort-and-filter.ts index e95fa9d..a5e95c2 100644 --- a/examples/sort-and-filter.ts +++ b/examples/sort-and-filter.ts @@ -1,8 +1,8 @@ import {printTable} from '../src/index.js' const data = [ - {age: 25, id: '10245', name: 'Bob'}, - {age: 26, id: '10345', name: 'Bill'}, + {age: 100, id: '10245', name: 'Bob'}, + {age: 10, id: '10345', name: 'Bill'}, {age: 30, id: '20245', name: 'Alice'}, {age: 20, id: '20345', name: 'Amy'}, {age: 30, id: '30245', name: 'Charlie'}, diff --git a/examples/styles.ts b/examples/styles.ts index 2ca60e5..7f22cc1 100644 --- a/examples/styles.ts +++ b/examples/styles.ts @@ -33,3 +33,16 @@ for (const borderStyle of BORDER_STYLES) { }) console.log() } + +printTable({ + borderStyle: 'all', + columns: ['id', {key: 'name', name: 'First Name'}, 'age'], + data, + headerOptions: { + formatter: 'capitalCase', + }, + horizontalAlignment: 'center', + noStyle: true, + title: 'Remove style with "noStyle: true"', + titleOptions: {bold: true}, +}) diff --git a/package.json b/package.json index 84186cf..541197a 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,9 @@ "description": "Display table in terminal", "version": "0.1.10", "author": "Salesforce", - "bugs": "https://github.com/oclif/multi-stage-output/issues", + "bugs": "https://github.com/oclif/table/issues", "dependencies": { "@oclif/core": "^4", - "@types/react": "^18.3.10", "change-case": "^5.4.4", "cli-truncate": "^4.0.0", "ink": "^5.0.1", @@ -23,6 +22,7 @@ "@types/mocha": "^10.0.8", "@types/node": "^18", "@types/object-hash": "^3.0.6", + "@types/react": "^18.3.10", "@types/sinon": "^17.0.3", "ansis": "^3.3.2", "chai": "^4.5.0", @@ -52,7 +52,7 @@ "files": [ "/lib" ], - "homepage": "https://github.com/oclif/core", + "homepage": "https://github.com/oclif/table", "keywords": [ "oclif", "cli", @@ -62,7 +62,7 @@ "exports": { ".": "./lib/index.js" }, - "repository": "oclif/core", + "repository": "oclif/table", "publishConfig": { "access": "public" }, diff --git a/src/skeletons.ts b/src/skeletons.ts index 320c607..6a65053 100644 --- a/src/skeletons.ts +++ b/src/skeletons.ts @@ -8,6 +8,7 @@ export const BORDER_STYLES = [ 'none', 'outline', 'vertical-with-outline', + 'vertical-rows-with-outline', 'vertical', ] as const @@ -337,6 +338,44 @@ export const BORDER_SKELETONS: Record< right: '', }, }, + 'vertical-rows-with-outline': { + data: { + cross: '│', + left: '│', + line: ' ', + right: '│', + }, + footer: { + cross: '┴', + left: '└', + line: '─', + right: '┘', + }, + header: { + cross: '─', + left: '┌', + line: '─', + right: '┐', + }, + headerFooter: { + cross: '┬', + left: '├', + line: '─', + right: '┤', + }, + heading: { + cross: ' ', + left: '│', + line: ' ', + right: '│', + }, + separator: { + cross: '', + left: '', + line: '', + right: '', + }, + }, 'vertical-with-outline': { data: { cross: '│', diff --git a/src/table.tsx b/src/table.tsx index b7f31fc..2988c93 100644 --- a/src/table.tsx +++ b/src/table.tsx @@ -128,7 +128,7 @@ function formatTextWithMargins({ const valueWithNoZeroWidthChars = String(value).replaceAll('​', ' ') const spaceForText = width - padding * 2 - if (stripAnsi(valueWithNoZeroWidthChars).length < spaceForText) { + if (stripAnsi(valueWithNoZeroWidthChars).length <= spaceForText) { const spaces = width - stripAnsi(valueWithNoZeroWidthChars).length return { text: valueWithNoZeroWidthChars, @@ -139,12 +139,42 @@ function formatTextWithMargins({ if (overflow === 'wrap') { const wrappedText = wrapAnsi(valueWithNoZeroWidthChars, spaceForText, {hard: true, trim: true, wordWrap: true}) const {marginLeft, marginRight} = calculateMargins(width - determineWidthOfWrappedText(stripAnsi(wrappedText))) - const text = wrappedText.replaceAll('\n', `${' '.repeat(marginRight)}\n${' '.repeat(marginLeft)}`) + + const lines = wrappedText.split('\n').map((line, idx) => { + const {marginLeft: lineSpecificLeftMargin} = calculateMargins(width - stripAnsi(line).length) + + if (horizontalAlignment === 'left') { + if (idx === 0) { + // if it's the first line, only add margin to the right side (The left margin will be applied later) + return `${line}${' '.repeat(marginRight)}` + } + + // if left alignment, add the overall margin to the left side and right sides + return `${' '.repeat(marginLeft)}${line}${' '.repeat(marginRight)}` + } + + if (horizontalAlignment === 'center') { + if (idx === 0) { + // if it's the first line, only add margin to the right side (The left margin will be applied later) + return `${line}${' '.repeat(marginRight)}` + } + + // if center alignment, add line specific margin to the left side and the overall margin to the right side + return `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(marginRight)}` + } + + // right alignment + if (idx === 0) { + return `${' '.repeat(Math.max(0, lineSpecificLeftMargin - marginLeft))}${line}${' '.repeat(marginRight)}` + } + + return `${' '.repeat(lineSpecificLeftMargin)}${line}${' '.repeat(marginRight)}` + }) return { marginLeft, marginRight, - text, + text: lines.join('\n'), } } @@ -163,7 +193,6 @@ export function Table>(props: TableOptions) horizontalAlignment = 'left', maxWidth, noStyle = false, - orientation = 'horizontal', overflow = 'truncate', padding = 1, sort, @@ -237,48 +266,6 @@ export function Table>(props: TableOptions) skeleton: BORDER_SKELETONS[config.borderStyle].separator, }) - if (orientation === 'vertical') { - return ( - - {title && {title}} - {processedData.map((row, index) => { - // Calculate the hash of the row based on its value and position - const key = `row-${sha1(row)}-${index}` - const maxKeyLength = Math.max(...Object.values(headings).map((c) => c.length)) - // Construct a row. - return ( - - {/* 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 ( - - - {keyName} - {keyPadding} - - {value} - - ) - })} - - ) - })} - - ) - } - return ( {title && {title}} @@ -428,8 +415,8 @@ export function printTables[]>( const processed = tables.map((table) => ({ ...table, - // adjust maxWidth to account for margin - maxWidth: determineConfiguredWidth(table.maxWidth, columns), + // adjust maxWidth to account for margin and columnGap + maxWidth: determineConfiguredWidth(table.maxWidth, columns) - (options?.columnGap ?? 0) * tables.length, })) const instance = render( diff --git a/src/types.ts b/src/types.ts index 0d89dfb..c5624de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -113,7 +113,7 @@ export type TableOptions> = { * * If you provide a number or percentage that is larger than the terminal width, it will default to the terminal width. * - * If you provide a number or percentage that is too small to fit the table, it will default to the width of the table. + * If you provide a number or percentage that is too small to fit the table, it will default to the minimum width of the table. */ maxWidth?: Percentage | number /** @@ -125,7 +125,7 @@ export type TableOptions> = { */ headerOptions?: HeaderOptions /** - * Border style for the table. Defaults to 'all'. Only applies to horizontal orientation. + * Border style for the table. Defaults to 'all'. */ borderStyle?: BorderStyle /** @@ -133,7 +133,7 @@ export type TableOptions> = { */ borderColor?: SupportedColor /** - * Align data in columns. Defaults to 'left'. Only applies to horizontal orientation. + * Align data in columns. Defaults to 'left'. */ horizontalAlignment?: HorizontalAlignment /** @@ -170,26 +170,7 @@ export type TableOptions> = { */ sort?: Sort /** - * The orientation of the table. Defaults to 'horizontal'. - * - * If 'vertical', individual records will be displayed vertically in key:value pairs. - * - * @example - * ``` - * ───────────── - * Name Alice - * Id 36329 - * Age 20 - * ───────────── - * Name Bob - * Id 49032 - * Age 21 - * ───────────── - * ``` - */ - orientation?: 'horizontal' | 'vertical' - /** - * Vertical alignment of cell content. Defaults to 'top'. Only applies to horizontal orientation. + * Vertical alignment of cell content. Defaults to 'top'. */ verticalAlignment?: VerticalAlignment /**