Skip to content

Commit

Permalink
fix: properly compute border boundaries & merge cell refs
Browse files Browse the repository at this point in the history
  • Loading branch information
ChronicStone committed Apr 22, 2024
1 parent 8431565 commit 237dbec
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 84 deletions.
Binary file modified example.xlsx
Binary file not shown.
57 changes: 38 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
}

const tableContentExtraRows = tableConfig.columns.reduce((acc, col) => {
return Math.max(acc, tableConfig.content.reduce((acc, row) => {
const _resolvedValue = col.value(row)
return Math.max(acc, tableConfig.content.reduce((acc, row, rowIndex) => {
const _resolvedValue = col.value(row, rowIndex)
const values = Array.isArray(_resolvedValue) ? _resolvedValue : [_resolvedValue]
return values.length - 1 + acc
}, 0))
Expand Down Expand Up @@ -207,12 +207,19 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {

tableConfig.content.forEach((row, rowIndex) => {
const maxRowHeight = tableConfig.columns.reduce((acc, column) => {
const _resolvedValue = column.value(row)
const _resolvedValue = column.value(row, rowIndex)
const values = Array.isArray(_resolvedValue) ? _resolvedValue : [_resolvedValue]
return Math.max(acc, values.length)
}, 1)

const _resolvedValue = column.value(row)
const prevRowHeight = tableConfig.columns.reduce((acc, column) => {
return Math.max(acc, tableConfig.content.filter((_, i) => i < rowIndex).reduce((acc, row, rowIndex) => {
const value = column.value(row, rowIndex)
return acc + (Array.isArray(value) ? value.length : 1)
}, 0))
}, 0)

const _resolvedValue = column.value(row, rowIndex)
const values = Array.isArray(_resolvedValue) ? _resolvedValue : [_resolvedValue]
const style = typeof column._ref.cellStyle === 'function'
? column._ref.cellStyle(row)
Expand All @@ -222,7 +229,11 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
: column._ref.format

values.forEach((value, valueIndex) => {
const cellRef = utils.encode_cell({ c: colIndex + COL_OFFSET, r: (rowIndex * maxRowHeight) + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) + valueIndex })
const cellRef = utils.encode_cell({
c: colIndex + COL_OFFSET,
r: prevRowHeight + ROW_OFFSET + (rowHasTitle ? 1 : 0) + (valueIndex + 1),
})

worksheet[cellRef] = {
v: value === null ? '' : value,
t: getCellDataType(value),
Expand All @@ -246,8 +257,11 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
})

if (values.length < maxRowHeight && maxRowHeight > 1) {
for (let i = values.length; i < maxRowHeight; i++) {
const cellRef = utils.encode_cell({ c: colIndex + COL_OFFSET, r: (rowIndex * maxRowHeight) + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) + i })
for (let valueIndex = values.length; valueIndex < maxRowHeight; valueIndex++) {
const cellRef = utils.encode_cell({
c: colIndex + COL_OFFSET,
r: prevRowHeight + ROW_OFFSET + (rowHasTitle ? 1 : 0) + (valueIndex + 1),
})
worksheet[cellRef] = {
v: '',
t: 's',
Expand All @@ -272,20 +286,21 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
worksheet['!merges'] = []

worksheet['!merges'].push({
s: { c: colIndex + COL_OFFSET, r: (rowIndex * maxRowHeight) + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) },
e: { c: colIndex + COL_OFFSET, r: (rowIndex * maxRowHeight) + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) + maxRowHeight - 1 },
s: { c: colIndex + COL_OFFSET, r: prevRowHeight + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) },
e: { c: colIndex + COL_OFFSET, r: prevRowHeight + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) + maxRowHeight - 1 },
})
}
}
// if (colIndex === 0)
// ROW_OFFSET += (maxRowHeight - 1)
})

if (tableHasSummary(tableConfig)) {
const summaryRowIndex = tableConfig.content.length + 1 + tableContentExtraRows
for (const summaryIndex in column._ref?.summary ?? []) {
const summary = column._ref?.summary?.[summaryIndex]
const cellRef = utils.encode_cell({ c: +colIndex + COL_OFFSET, r: summaryRowIndex + ROW_OFFSET + +summaryIndex + (rowHasTitle ? 1 : 0) })
const cellRef = utils.encode_cell({
c: +colIndex + COL_OFFSET,
r: summaryRowIndex + ROW_OFFSET + +summaryIndex + (rowHasTitle ? 1 : 0),
})
if (!summary) {
worksheet[cellRef] = {
v: '',
Expand Down Expand Up @@ -330,23 +345,27 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
}
})

// worksheet['!merges'].push({
// s: { prevRowHeight + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) },
// e: { c: colIndex + COL_OFFSET, r: prevRowHeight + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0) + maxRowHeight - 1 },
// })

if (tableContentExtraRows > 0) {
tableConfig.content.forEach((row, rowIndex) => {
const prevRowsExtraHeight = tableConfig.columns.reduce((acc, col) => {
return Math.max(acc, tableConfig.content.filter((_, i) => i < rowIndex).reduce((acc, row) => {
const value = col.value(row)

const prevRowHeight = tableConfig.columns.reduce((acc, column) => {
return Math.max(acc, tableConfig.content.filter((_, i) => i < rowIndex).reduce((acc, row, rowIndex) => {
const value = column.value(row, rowIndex)
return acc + (Array.isArray(value) ? value.length : 1)
}, 0))
}, 0)
const rowStart = ROW_OFFSET + (rowHasTitle ? 1 : 0) + prevRowsExtraHeight + 1
const rowStart = prevRowHeight + 1 + ROW_OFFSET + (rowHasTitle ? 1 : 0)
const currentRowHeight = tableConfig.columns.reduce((acc, col) => {
const value = col.value(row)
const value = col.value(row, rowIndex)
return Math.max(acc, Array.isArray(value) ? value.length : 1)
}, 1)

const start = utils.encode_cell({ c: COL_OFFSET, r: rowStart })
const end = utils.encode_cell({ c: COL_OFFSET + tableConfig.columns.length - 1, r: rowStart + currentRowHeight - 1 })
const end = utils.encode_cell({ c: COL_OFFSET + tableConfig.columns.length - 1, r: rowStart + (currentRowHeight - 1) })
applyGroupBorders(worksheet, { start, end })
})
}
Expand Down
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type Prettify<T> = {
export type BaseCellValue = string | number | boolean | null | undefined | Date
export type CellValue = BaseCellValue | BaseCellValue[]

export type ValueTransformer = (value: any) => CellValue
export type ValueTransformer = (value: any, index: number) => CellValue

export interface TransformersMap {
[key: string]: ValueTransformer
Expand Down Expand Up @@ -93,8 +93,8 @@ export type Column<
}>
} & (
ExtractColumnValue<T, FieldValue> extends CellValue
? { transform?: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>) => CellValue) }
: { transform: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>) => CellValue) }
? { transform?: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>, index: number) => CellValue) }
: { transform: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>, index: number) => CellValue) }
)

export interface ColumnGroup<
Expand Down
20 changes: 8 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function buildSheetConfig(sheets: Array<SheetConfig>) {
.map((column) => {
return {
label: column?.label ?? formatKey(column.columnKey),
value: (row: GenericObject): CellValue => {
value: (row: GenericObject, index: number): CellValue => {
const value = typeof column.key === 'string'
? getPropertyFromPath(row, column.key)
: column.key(row)
Expand All @@ -88,7 +88,8 @@ export function buildSheetConfig(sheets: Array<SheetConfig>) {
)
return column.default

return column.transform ? (column.transform as ValueTransformer)(value) : value
const transformedVal = column.transform ? (column.transform as ValueTransformer)(value, index) : value
return (Array.isArray(transformedVal) && !transformedVal.length) ? null : transformedVal
},
_ref: column,
}
Expand All @@ -110,12 +111,7 @@ export function getColumnHeaderStyle(params: { bordered: boolean }) {
alignment: { horizontal: 'center', vertical: 'center' },
fill: { fgColor: { rgb: 'E9E9E9' } },
border: (params?.bordered ?? true)
? {
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } },
top: { style: 'thin', color: { rgb: '000000' } },
}
? THICK_BORDER_STYLE
: {},
} satisfies CellStyle
}
Expand Down Expand Up @@ -187,9 +183,9 @@ export function getSheetChunkMaxHeight(
const summaryRowLength = tableSummaryRowLength(table)

// Calculate the maximum row span needed for any row within this table using .reduce
const maxRowSpan = table.content.reduce((max, row) => {
const maxRowSpan = table.content.reduce((max, row, rowIndex) => {
return Math.max(max, ...table.columns.map((column) => {
const values = column.value(row)
const values = column.value(row, rowIndex)
return Array.isArray(values) ? values.length : 1
}))
}, 1) // Start with 1 as the minimum span
Expand Down Expand Up @@ -229,8 +225,8 @@ export function computeSheetRange(sheetRows: Array<ReturnType<typeof buildSheetC

// Compute max row span due to multi-value columns
const maxRowSpan = table.columns.reduce((max, column) => {
return Math.max(max, table.content.reduce((maxRow, row) => {
const values = column.value(row)
return Math.max(max, table.content.reduce((maxRow, row, rowIndex) => {
const values = column.value(row, rowIndex)
return Array.isArray(values) ? Math.max(maxRow, values.length) : maxRow
}, 1))
}, 1)
Expand Down
99 changes: 49 additions & 50 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ describe('should generate the example excel', () => {
key: 'id',
summary: [{ value: () => 'TOTAL BEFORE VAT' }, { value: () => 'TOTAL' }],
})
.column('firstName', { key: 'firstName', transform: () => ['Cyprien', 'THAO'] })
.column('lastName', { key: 'lastName', transform: () => ['Thao', 'Other', 'Test'] })
.column('lastName', { key: 'lastName', transform: (_, i) => i === 0 ? [] : i === 1 ? ['Cyp', 'THAO'] : ['OTHER'] })
.column('email', { key: 'email' })
.column('roles', {
key: 'roles',
Expand Down Expand Up @@ -130,54 +129,54 @@ describe('should generate the example excel', () => {
'group:org': organizations,
},
})
// .sheet('Users - partial')
// .addTable({
// data: users,
// schema: assessmentExport,
// select: {
// firstName: true,
// lastName: true,
// email: true,
// },
// })
// .sheet('User - neg partial')
// .addTable({
// data: users,
// schema: assessmentExport,
// select: {
// firstName: false,
// lastName: false,
// email: false,
// },
// context: {
// 'group:org': organizations,
// },
// })
// .sheet('Multi-tables-grid', { tablesPerRow: 2 })
// .addTable({
// title: 'Table 1',
// data: users.filter((_, i) => i < 5),
// schema: assessmentExport,
// select: { firstName: true, lastName: true, email: true, createdAt: true },
// })
// .addTable({
// title: 'Table 2',
// data: users.filter((_, i) => i < 5),
// schema: assessmentExport,
// select: { firstName: true, lastName: true, email: true, balance: true },
// })
// .addTable({
// title: 'Table 3',
// data: users.filter((_, i) => i < 5),
// schema: assessmentExport,
// select: { firstName: true, lastName: true, email: true, balance: true },
// })
// .addTable({
// title: 'Table 4',
// data: users.filter((_, i) => i < 5),
// schema: assessmentExport,
// select: { firstName: true, lastName: true, email: true, createdAt: true },
// })
.sheet('Users - partial')
.addTable({
data: users,
schema: assessmentExport,
select: {
firstName: true,
lastName: true,
email: true,
},
})
.sheet('User - neg partial')
.addTable({
data: users,
schema: assessmentExport,
select: {
firstName: false,
lastName: false,
email: false,
},
context: {
'group:org': organizations,
},
})
.sheet('Multi-tables-grid', { tablesPerRow: 2 })
.addTable({
title: 'Table 1',
data: users.filter((_, i) => i < 5),
schema: assessmentExport,
select: { firstName: true, lastName: true, email: true, createdAt: true },
})
.addTable({
title: 'Table 2',
data: users.filter((_, i) => i < 5),
schema: assessmentExport,
select: { firstName: true, lastName: true, email: true, balance: true },
})
.addTable({
title: 'Table 3',
data: users.filter((_, i) => i < 5),
schema: assessmentExport,
select: { firstName: true, lastName: true, email: true, balance: true },
})
.addTable({
title: 'Table 4',
data: users.filter((_, i) => i < 5),
schema: assessmentExport,
select: { firstName: true, lastName: true, email: true, createdAt: true },
})
.build({ output: 'buffer' })

fs.writeFileSync('example.xlsx', buffer)
Expand Down

0 comments on commit 237dbec

Please sign in to comment.