Skip to content

Commit

Permalink
feat: add column label property & cell styling control
Browse files Browse the repository at this point in the history
  • Loading branch information
ChronicStone committed Dec 3, 2023
1 parent 227a528 commit 18dbd82
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 33 deletions.
1 change: 0 additions & 1 deletion .github/FUNDING.yml

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"prepare": "simple-git-hooks"
},
"dependencies": {
"deepmerge-ts": "^5.1.0",
"json-as-xlsx": "^2.5.6",
"xlsx-js-style": "^1.2.0"
},
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 50 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import xlsx, { type IColumn, type IJsonSheet } from 'json-as-xlsx'
import type { CellStyle } from 'xlsx-js-style'
import XLSX from 'xlsx-js-style'
import { deepmerge } from 'deepmerge-ts'
import type { CellValue, Column, ExcelSchema, GenericObject, NestedPaths, Not, Sheet, TransformersMap, ValueTransformer } from './types'
import { getPropertyFromPath } from './utils'
import { getPropertyFromPath, getSheetCellKey } from './utils'

export class ExcelSchemaBuilder<
T extends GenericObject,
Expand Down Expand Up @@ -70,16 +73,17 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
.filter(column => !sheet.select || sheet.select.includes(column.columnKey))
.map((column) => {
return {
label: column.columnKey,
label: column?.label ?? column.columnKey,
value: (row) => {
const value = typeof column.value === 'string'
? getPropertyFromPath(row, column.value)
: column.value(row)
const value = typeof column.key === 'string'
? getPropertyFromPath(row, column.key)
: column.key(row)

if (!value)
return column.default ?? ''
return column.transform ? (column.transform as ValueTransformer)(value) : value
},
format: column.format,
} satisfies IColumn
}),
content: sheet.data,
Expand All @@ -95,7 +99,47 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
// eslint-disable-next-line node/prefer-global/buffer
}) as Buffer

return fileBody
const workbook = XLSX.read(fileBody, { type: 'buffer' })
workbook.SheetNames.forEach((sheetName) => {
const sheetConfig = this.sheets.find(sheet => sheet.sheetKey === sheetName)
if (!sheetConfig)
return

sheetConfig.schema.forEach((column, index) => {
const headerCellRef = getSheetCellKey(index + 1, 1)
if (!workbook.Sheets[sheetName][headerCellRef])
return
workbook.Sheets[sheetName][headerCellRef].s = {
font: { bold: true },
alignment: { horizontal: 'center' },
fill: { fgColor: { rgb: 'E9E9E9' } },
border: {
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } },
top: { style: 'thin', color: { rgb: '000000' } },
},
} satisfies CellStyle
sheetConfig.data.forEach((row, rowIndex) => {
const cellRef = getSheetCellKey(index + 1, rowIndex + 2)
const style = column.cellStyle?.(row) ?? {}
workbook.Sheets[sheetName][cellRef].s = deepmerge(
style,
{
border: {
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } },
top: { style: 'thin', color: { rgb: '000000' } },
},
},
)
})
})
})

// eslint-disable-next-line node/prefer-global/buffer
return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer
// for()
}
}
28 changes: 16 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import type { CellStyle } from 'xlsx-js-style'

export type GenericObject = Record<string | number | symbol, any>

export type NestedPaths<T> = T extends Array<infer U>
? U extends object ? never : never
: T extends object
? {
[K in keyof T & (string | number)]: K extends string
? `${K}` | (NonNullable<T[K]> extends object ? `${K}.${NestedPaths<NonNullable<T[K]>>}` : never)
: never;
}[keyof T & (string | number)]
: never
? U extends (object | Date) ? never : never
: T extends Date ? never
: T extends object
? {
[K in keyof T & (string | number)]: K extends string
? `${K}` | (NonNullable<T[K]> extends object ? `${K}.${NestedPaths<NonNullable<T[K]>>}` : never)
: never;
}[keyof T & (string | number)]
: never

export type NestedPathsForType<T, P> = T extends Array<infer U>
? U extends object ? never : never
Expand Down Expand Up @@ -50,7 +53,7 @@ export type DeepRequired<T> = {
}

export type TypedTransformersMap<TransformMap extends TransformersMap, Value> = {
[K in keyof TransformMap]: Parameters<TransformMap[K]>[0] extends Value ? K : never;
[K in keyof TransformMap]: Value extends Parameters<TransformMap[K]>[0] ? K : never;
}[keyof TransformMap]

export type ExtractColumnValue<
Expand All @@ -63,12 +66,13 @@ export type Column<
FieldValue extends string | ((data: T) => CellValue),
ColKey extends string,
TransformMap extends TransformersMap,

> = {
label?: string
columnKey: ColKey
value: FieldValue
key: FieldValue
default?: CellValue
// test?: FieldValue extends string ? TypeFromPath<T, FieldValue> : FieldValue extends (data: T) => CellValue ? ReturnType<FieldValue> : never
format?: string
cellStyle?: (rowData: T) => CellStyle
} & (
ExtractColumnValue<T, FieldValue> extends CellValue
? { transform?: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>) => CellValue) }
Expand Down
Binary file removed test.xlsx
Binary file not shown.
32 changes: 18 additions & 14 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ interface User {
technical: { overall: number }
interview?: { overall: number }
}
createdAt: Date
}

const transformers = {
boolean: (value: boolean) => value ? 'Yes' : 'No',
list: (value: (string)[]) => value.join(', '),
arrayLength: (value: any[]) => value.length,
boolean: (key: boolean) => key ? 'Yes' : 'No',
list: (key: (string)[]) => key.join(', '),
arrayLength: (key: any[] | string) => key.length,
date: (key: Date) => key.toLocaleDateString(),
} satisfies TransformersMap
// Usage example

describe('should', () => {
it('exported', () => {
const users: User[] = Array.from({ length: 100 }, (_, id) => ({
const users: User[] = Array.from({ length: 10 }, (_, id) => ({
id,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
Expand All @@ -48,20 +50,22 @@ describe('should', () => {
technical: { overall: Math.floor(Math.random() * 10) },
...(Math.random() > 0.5 ? { interview: { overall: Math.floor(Math.random() * 10) } } : {}),
},
createdAt: faker.date.past(),
}))
const assessmentExport = ExcelSchemaBuilder
.create<User>()
.withTransformers(transformers)
.column('id', { value: 'id' })
.column('firstName', { value: 'firstName' })
.column('lastName', { value: 'lastName' })
.column('email', { value: 'email' })
.column('roles', { value: 'roles', transform: 'list' })
.column('nbOrgs', { value: 'organizations', transform: 'arrayLength' })
.column('orgs', { value: 'organizations', transform: value => value.map(org => org.name).join(', ') })
.column('generalScore', { value: 'results.general.overall' })
.column('technicalScore', { value: 'results.technical.overall' })
.column('interviewScore', { value: 'results.interview.overall', default: 'N/A' })
.column('id', { key: 'id' })
.column('firstName', { key: 'firstName', cellStyle: () => ({ fill: { fgColor: { rgb: 'E9E9E9' } } }) })
.column('lastName', { key: 'lastName' })
.column('email', { key: 'email' })
.column('roles', { key: 'roles', transform: 'list' })
.column('nbOrgs', { key: 'organizations', transform: 'arrayLength' })
.column('orgs', { key: 'organizations', transform: org => org.map(org => org.name).join(', ') })
.column('generalScore', { key: 'results.general.overall' })
.column('technicalScore', { key: 'results.technical.overall' })
.column('interviewScore', { key: 'results.interview.overall', default: 'N/A' })
.column('createdAt', { key: 'createdAt', transform: 'date' })
.build()

const buffer = ExcelBuilder
Expand Down

0 comments on commit 18dbd82

Please sign in to comment.