Skip to content

Commit f7dc5e2

Browse files
committed
feat: added description to generated sample excel file
1 parent 025d107 commit f7dc5e2

File tree

4 files changed

+291
-9
lines changed

4 files changed

+291
-9
lines changed

apps/api/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"test": "cross-env TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --timeout 10000 --require ts-node/register --exit src/**/**/*.spec.ts"
2020
},
2121
"dependencies": {
22-
"@impler/dal": "^0.24.1",
23-
"@impler/services": "^0.24.1",
24-
"@impler/shared": "^0.24.1",
22+
"@eyeseetea/xlsx-populate": "^4.3.0",
23+
"@impler/dal": "workspace:^",
24+
"@impler/services": "workspace:^",
25+
"@impler/shared": "workspace:^",
2526
"@nestjs/common": "^9.1.2",
2627
"@nestjs/core": "^9.1.2",
2728
"@nestjs/jwt": "^10.0.1",

apps/api/src/app/shared/services/file/file.service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as XLSX from 'xlsx';
22
import { cwd } from 'node:process';
3-
import * as xlsxPopulate from 'xlsx-populate';
3+
import * as xlsxPopulate from '@eyeseetea/xlsx-populate';
44
import { CONSTANTS } from '@shared/constants';
55
import { ParseConfig, parse } from 'papaparse';
66
import { ColumnDelimiterEnum, ColumnTypesEnum, Defaults, FileEncodingsEnum } from '@impler/shared';
@@ -90,6 +90,14 @@ export class ExcelFileService {
9090
multiSelectHeadings[heading.key] = heading.delimiter || ColumnDelimiterEnum.COMMA;
9191
} else worksheet.cell(columnHeadingCellName).value(heading.key);
9292
worksheet.column(columnName).style('numberFormat', '@');
93+
if (heading.description)
94+
worksheet.cell(columnHeadingCellName).comment({
95+
text: heading.description,
96+
width: '200px',
97+
height: '100px',
98+
textAlign: 'left',
99+
horizontalAlignment: 'Left',
100+
});
93101
});
94102

95103
const frozenColumns = headings.filter((heading) => heading.isFrozen).length;
@@ -139,7 +147,7 @@ export class ExcelFileService {
139147
}
140148
const buffer = await workbook.outputAsync();
141149

142-
return buffer as Promise<Buffer>;
150+
return buffer;
143151
}
144152
getExcelSheets(file: Express.Multer.File): Promise<string[]> {
145153
return new Promise(async (resolve, reject) => {
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import * as XLSX from 'xlsx';
2+
import { cwd } from 'node:process';
3+
import * as xlsxPopulate from 'xlsx-populate';
4+
import { CONSTANTS } from '@shared/constants';
5+
import { ParseConfig, parse } from 'papaparse';
6+
import { ColumnDelimiterEnum, ColumnTypesEnum, Defaults, FileEncodingsEnum } from '@impler/shared';
7+
import { EmptyFileException } from '@shared/exceptions/empty-file.exception';
8+
import { InvalidFileException } from '@shared/exceptions/invalid-file.exception';
9+
import { IExcelFileHeading } from '@shared/types/file.types';
10+
11+
export class ExcelFileService {
12+
async convertToCsv(file: Express.Multer.File, sheetName?: string): Promise<string> {
13+
return new Promise(async (resolve, reject) => {
14+
try {
15+
const wb = XLSX.read(file.buffer);
16+
const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]];
17+
resolve(
18+
XLSX.utils.sheet_to_csv(ws, {
19+
blankrows: false,
20+
skipHidden: true,
21+
forceQuotes: true,
22+
// rawNumbers: true, // was converting 12:12:12 to 1.3945645673
23+
})
24+
);
25+
} catch (error) {
26+
reject(error);
27+
}
28+
});
29+
}
30+
formatName(name: string): string {
31+
return (
32+
CONSTANTS.EXCEL_DATA_SHEET_STARTER +
33+
name
34+
.replace(/[^a-zA-Z0-9]/g, '')
35+
.toLowerCase()
36+
.slice(0, 25) // exceljs don't allow heading more than 30 characters
37+
);
38+
}
39+
addSelectSheet(wb: any, heading: IExcelFileHeading): string {
40+
const name = this.formatName(heading.key);
41+
const addedSheet = wb.addSheet(name);
42+
addedSheet.cell('A1').value(heading.key);
43+
heading.selectValues.forEach((value, index) => addedSheet.cell(`A${index + 2}`).value(value));
44+
45+
return name;
46+
}
47+
48+
getExcelColumnNameFromIndex(columnNumber: number) {
49+
// To store result (Excel column name)
50+
const columnName = [];
51+
52+
while (columnNumber > 0) {
53+
// Find remainder
54+
const rem = columnNumber % 26;
55+
56+
/*
57+
* If remainder is 0, then a
58+
* 'Z' must be there in output
59+
*/
60+
if (rem == 0) {
61+
columnName.push('Z');
62+
columnNumber = Math.floor(columnNumber / 26) - 1;
63+
} // If remainder is non-zero
64+
else {
65+
columnName.push(String.fromCharCode(rem - 1 + 'A'.charCodeAt(0)));
66+
columnNumber = Math.floor(columnNumber / 26);
67+
}
68+
}
69+
70+
return columnName.reverse().join('');
71+
}
72+
async getExcelFileForHeadings(headings: IExcelFileHeading[], data?: string): Promise<Buffer> {
73+
const currentDir = cwd();
74+
const isMultiSelect = headings.some(
75+
(heading) => heading.type === ColumnTypesEnum.SELECT && heading.allowMultiSelect
76+
);
77+
const workbook = await xlsxPopulate.fromFileAsync(
78+
`${currentDir}/src/config/${isMultiSelect ? 'Excel Multi Select Template.xlsm' : 'Excel Template.xlsx'}`
79+
);
80+
const worksheet = workbook.sheet('Data');
81+
const multiSelectHeadings = {};
82+
83+
headings.forEach((heading, index) => {
84+
const columnName = this.getExcelColumnNameFromIndex(index + 1);
85+
const columnHeadingCellName = columnName + '1';
86+
if (heading.type === ColumnTypesEnum.SELECT && heading.allowMultiSelect) {
87+
worksheet
88+
.cell(columnHeadingCellName)
89+
.value(heading.key + '#MULTI' + '#' + (heading.delimiter || ColumnDelimiterEnum.COMMA));
90+
multiSelectHeadings[heading.key] = heading.delimiter || ColumnDelimiterEnum.COMMA;
91+
} else worksheet.cell(columnHeadingCellName).value(heading.key);
92+
worksheet.column(columnName).style('numberFormat', '@');
93+
});
94+
95+
const frozenColumns = headings.filter((heading) => heading.isFrozen).length;
96+
if (frozenColumns) worksheet.freezePanes(frozenColumns, 1); // freeze panes (freeze first n column and first row)
97+
else worksheet.freezePanes(0, 1); // freeze 0 column and first row
98+
99+
headings.forEach((heading, index) => {
100+
if (heading.type === ColumnTypesEnum.SELECT) {
101+
const keyName = this.addSelectSheet(workbook, heading);
102+
const columnName = this.getExcelColumnNameFromIndex(index + 1);
103+
worksheet.range(`${columnName}2:${columnName}9999`).dataValidation({
104+
type: 'list',
105+
allowBlank: !heading.isRequired,
106+
formula1: `${keyName}!$A$2:$A$9999`,
107+
...(!heading.allowMultiSelect
108+
? {
109+
showErrorMessage: true,
110+
error: 'Please select from the list',
111+
errorTitle: 'Invalid Value',
112+
}
113+
: {}),
114+
});
115+
}
116+
});
117+
const headingNames = headings.map((heading) => heading.key);
118+
const endColumnPosition = this.getExcelColumnNameFromIndex(headings.length + 1);
119+
120+
let parsedData = [];
121+
try {
122+
if (data) parsedData = JSON.parse(data);
123+
} catch (error) {}
124+
if (Array.isArray(parsedData) && parsedData.length > 0) {
125+
const rows: string[][] = parsedData.reduce<string[][]>((acc: string[][], rowItem: Record<string, any>) => {
126+
acc.push(
127+
headingNames.map((headingKey) =>
128+
multiSelectHeadings[headingKey] && Array.isArray(rowItem[headingKey])
129+
? rowItem[headingKey].join(multiSelectHeadings[headingKey])
130+
: rowItem[headingKey]
131+
)
132+
);
133+
134+
return acc;
135+
}, []);
136+
const rangeKey = `A2:${endColumnPosition}${rows.length + 1}`;
137+
const range = workbook.sheet(0).range(rangeKey);
138+
range.value(rows);
139+
}
140+
const buffer = await workbook.outputAsync();
141+
142+
return buffer as Promise<Buffer>;
143+
}
144+
getExcelSheets(file: Express.Multer.File): Promise<string[]> {
145+
return new Promise(async (resolve, reject) => {
146+
try {
147+
const wb = XLSX.read(file.buffer);
148+
resolve(wb.SheetNames);
149+
} catch (error) {
150+
reject(error);
151+
}
152+
});
153+
}
154+
getExcelRowsColumnsCount(file: Express.Multer.File, sheetName?: string): Promise<{ rows: number; columns: number }> {
155+
return new Promise(async (resolve, reject) => {
156+
try {
157+
const wb = XLSX.read(file.buffer);
158+
const ws = sheetName && wb.SheetNames.includes(sheetName) ? wb.Sheets[sheetName] : wb.Sheets[wb.SheetNames[0]];
159+
const range = ws['!ref'];
160+
const regex = /([A-Z]+)(\d+):([A-Z]+)(\d+)/;
161+
const match = range.match(regex);
162+
163+
if (!match) reject(new InvalidFileException());
164+
165+
const [, startCol, startRow, endCol, endRow] = match;
166+
167+
function columnToNumber(col: string) {
168+
let num = 0;
169+
for (let i = 0; i < col.length; i++) {
170+
num = num * 26 + (col.charCodeAt(i) - 64);
171+
}
172+
173+
return num;
174+
}
175+
176+
const columns = columnToNumber(endCol) - columnToNumber(startCol) + 1;
177+
const rows = parseInt(endRow) - parseInt(startRow) + 1;
178+
179+
resolve({
180+
columns,
181+
rows,
182+
});
183+
} catch (error) {
184+
reject(error);
185+
}
186+
});
187+
}
188+
}
189+
190+
export class CSVFileService2 {
191+
getCSVMetaInfo(file: string | Express.Multer.File, options?: ParseConfig) {
192+
return new Promise<{ rows: number; columns: number }>((resolve, reject) => {
193+
let fileContent = '';
194+
if (typeof file === 'string') {
195+
fileContent = file;
196+
} else {
197+
fileContent = file.buffer.toString(FileEncodingsEnum.CSV);
198+
}
199+
let rows = 0;
200+
let columns = 0;
201+
202+
parse(fileContent, {
203+
...(options || {}),
204+
dynamicTyping: false,
205+
skipEmptyLines: true,
206+
step: function (results) {
207+
rows++;
208+
if (Array.isArray(results.data)) {
209+
columns = results.data.length;
210+
}
211+
},
212+
complete: function () {
213+
resolve({ rows, columns });
214+
},
215+
error: (error) => {
216+
if (error.message.includes('Parse Error')) {
217+
reject(new InvalidFileException());
218+
} else {
219+
reject(error);
220+
}
221+
},
222+
});
223+
});
224+
}
225+
226+
getFileHeaders(file: string | Express.Multer.File, options?: ParseConfig): Promise<string[]> {
227+
return new Promise((resolve, reject) => {
228+
let fileContent = '';
229+
if (typeof file === 'string') {
230+
fileContent = file;
231+
} else {
232+
fileContent = file.buffer.toString(FileEncodingsEnum.CSV);
233+
}
234+
let headings: string[];
235+
let recordIndex = -1;
236+
parse(fileContent, {
237+
...(options || {}),
238+
preview: 2,
239+
step: (results) => {
240+
recordIndex++;
241+
if (recordIndex === Defaults.ZERO) {
242+
if (Array.isArray(results.data) && results.data.length > Defaults.ZERO) headings = results.data as string[];
243+
else reject(new EmptyFileException());
244+
} else resolve(headings);
245+
},
246+
error: (error) => {
247+
if (error.message.includes('Parse Error')) {
248+
reject(new InvalidFileException());
249+
} else {
250+
reject(error);
251+
}
252+
},
253+
complete: () => {
254+
if (recordIndex !== Defaults.ONE) {
255+
reject(new EmptyFileException());
256+
}
257+
},
258+
});
259+
});
260+
}
261+
}

pnpm-lock.yaml

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)