Skip to content

Commit beea5d6

Browse files
authored
fix: csv and xlsx import (#315)
1 parent df742fa commit beea5d6

4 files changed

Lines changed: 216 additions & 43 deletions

File tree

package-lock.json

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@fileverse-dev/dsheet",
33
"private": false,
44
"description": "DSheet",
5-
"version": "1.2.98",
5+
"version": "1.2.99",
66
"main": "dist/index.es.js",
77
"module": "dist/index.es.js",
88
"exports": {
@@ -35,7 +35,7 @@
3535
"dependencies": {
3636
"@fileverse-dev/dsheets-templates": "^0.0.29",
3737
"@fileverse-dev/formulajs": "4.4.49",
38-
"@fileverse-dev/fortune-react": "1.3.0",
38+
"@fileverse-dev/fortune-react": "^1.3.1",
3939
"@fileverse/ui": "^5.0.0",
4040
"classnames": "^2.5.1",
4141
"exceljs": "^4.4.0",
@@ -79,4 +79,4 @@
7979
"typescript": "^5.2.2",
8080
"vite": "^5.0.0"
8181
}
82-
}
82+
}

package/hooks/use-xlsx-import.tsx

Lines changed: 176 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,137 @@ export const useXLSXImport = ({
9393
//@ts-expect-error, later
9494
await workbook.xlsx.load(arrayBuffer);
9595
const worksheet = workbook.getWorksheet(1);
96+
// Extract hyperlinks, freeze info, and cell formatting from all worksheets
97+
const hyperlinksBySheet: Record<
98+
number,
99+
Record<string, { linkType: string; linkAddress: string }>
100+
> = {};
101+
const frozenBySheet: Record<
102+
number,
103+
{
104+
type:
105+
| 'row'
106+
| 'column'
107+
| 'both'
108+
| 'rangeRow'
109+
| 'rangeColumn'
110+
| 'rangeBoth';
111+
range: { row_focus: number; column_focus: number };
112+
}
113+
> = {};
114+
const cellStylesBySheet: Record<
115+
number,
116+
Record<
117+
string,
118+
{
119+
bl?: number;
120+
it?: number;
121+
fs?: number;
122+
ff?: string;
123+
fc?: string;
124+
bg?: string;
125+
un?: number;
126+
}
127+
>
128+
> = {};
129+
workbook.eachSheet((ws, sheetIndex) => {
130+
const idx = sheetIndex - 1; // exceljs is 1-based
131+
132+
// Hyperlinks
133+
const sheetHyperlinks: Record<
134+
string,
135+
{ linkType: string; linkAddress: string }
136+
> = {};
137+
138+
// Freeze panes from worksheet views
139+
const views = ws.views;
140+
if (views && views.length > 0) {
141+
const view = views[0];
142+
if (view.state === 'frozen') {
143+
const xSplit = view.xSplit || 0;
144+
const ySplit = view.ySplit || 0;
145+
let type: 'rangeRow' | 'rangeColumn' | 'rangeBoth' | null = null;
146+
if (xSplit > 0 && ySplit > 0) type = 'rangeBoth';
147+
else if (ySplit > 0) type = 'rangeRow';
148+
else if (xSplit > 0) type = 'rangeColumn';
149+
if (type) {
150+
frozenBySheet[idx] = {
151+
type,
152+
range: {
153+
row_focus: ySplit - 1,
154+
column_focus: xSplit - 1,
155+
},
156+
};
157+
}
158+
}
159+
}
160+
161+
// Cell-level formatting and hyperlinks
162+
const styles: Record<
163+
string,
164+
{
165+
bl?: number;
166+
it?: number;
167+
fs?: number;
168+
ff?: string;
169+
fc?: string;
170+
bg?: string;
171+
un?: number;
172+
}
173+
> = {};
174+
ws.eachRow({ includeEmpty: false }, (row, rowNumber) => {
175+
row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
176+
const key = `${rowNumber - 1}_${colNumber - 1}`;
177+
178+
if (cell.hyperlink) {
179+
sheetHyperlinks[key] = {
180+
linkType: 'webpage',
181+
linkAddress: cell.hyperlink,
182+
};
183+
}
184+
185+
// Extract cell formatting
186+
const font = cell.style?.font;
187+
const fill = cell.style?.fill;
188+
const cellStyle: Record<string, string | number> = {};
189+
190+
if (font) {
191+
if (font.bold) cellStyle.bl = 1;
192+
if (font.italic) cellStyle.it = 1;
193+
if (font.underline) cellStyle.un = 1;
194+
if (font.size) cellStyle.fs = font.size;
195+
if (font.name) cellStyle.ff = font.name;
196+
if (font.color?.argb) {
197+
const argb = font.color.argb;
198+
const hex = '#' + argb.substring(argb.length - 6);
199+
cellStyle.fc = hex;
200+
}
201+
}
202+
if (
203+
fill?.type === 'pattern' &&
204+
fill.pattern === 'solid' &&
205+
fill.fgColor
206+
) {
207+
if (fill.fgColor.argb) {
208+
const argb = fill.fgColor.argb;
209+
const hex = '#' + argb.substring(argb.length - 6);
210+
cellStyle.bg = hex;
211+
}
212+
}
213+
if (Object.keys(cellStyle).length > 0) {
214+
styles[key] = cellStyle;
215+
}
216+
});
217+
});
218+
219+
if (Object.keys(sheetHyperlinks).length > 0) {
220+
hyperlinksBySheet[idx] = sheetHyperlinks;
221+
}
222+
if (Object.keys(styles).length > 0) {
223+
cellStylesBySheet[idx] = styles;
224+
}
225+
});
226+
96227
dropdownInfo =
97228
worksheet
98229
?.getSheetValues()
@@ -154,7 +285,7 @@ export const useXLSXImport = ({
154285
}
155286
const sheetArray = ydocRef.current.getArray(dsheetId);
156287
const localSheetsArray = Array.from(sheetArray) as Sheet[];
157-
sheets = sheets.map((sheet) => {
288+
sheets = sheets.map((sheet, sheetIndex) => {
158289
const lastCell = sheet?.celldata?.[sheet.celldata.length - 1];
159290

160291
const lastRow = lastCell?.r ?? 0;
@@ -164,31 +295,63 @@ export const useXLSXImport = ({
164295
sheet.column = Math.max(lastCol, 36);
165296

166297
if (!sheet.id) {
167-
sheet.id = sheetEditorRef.current?.getSettings().generateSheetId();
298+
sheet.id = sheetEditorRef.current
299+
?.getSettings()
300+
.generateSheetId();
168301
}
169302
if (sheet.calcChain) {
170303
sheet.calcChain = sheet.calcChain.map((chain) => {
171-
delete chain.id
172-
delete chain.index
173-
chain.id = sheet.id
174-
return chain
175-
})
304+
delete chain.id;
305+
delete chain.index;
306+
chain.id = sheet.id;
307+
return chain;
308+
});
309+
}
310+
// Attach freeze pane info
311+
if (frozenBySheet[sheetIndex]) {
312+
sheet.frozen = frozenBySheet[sheetIndex];
313+
}
314+
// Attach hyperlinks extracted from exceljs for this sheet
315+
if (hyperlinksBySheet[sheetIndex]) {
316+
sheet.hyperlink = {
317+
...(sheet.hyperlink || {}),
318+
...hyperlinksBySheet[sheetIndex],
319+
};
176320
}
177-
return sheet
178-
})
321+
// Apply cell formatting from exceljs (hyperlink styling, bold, italic, bg, etc.)
322+
const hlKeys = hyperlinksBySheet[sheetIndex];
323+
const styleKeys = cellStylesBySheet[sheetIndex];
324+
if ((hlKeys || styleKeys) && sheet.celldata) {
325+
for (const cell of sheet.celldata) {
326+
const key = `${cell.r}_${cell.c}`;
327+
if (cell.v) {
328+
// Apply formatting extracted from exceljs
329+
if (styleKeys?.[key]) {
330+
Object.assign(cell.v, styleKeys[key]);
331+
}
332+
// Override font color + underline for hyperlink cells
333+
if (hlKeys?.[key]) {
334+
cell.v.fc = 'rgb(0, 0, 255)';
335+
cell.v.un = 1;
336+
}
337+
}
338+
}
339+
}
340+
return sheet;
341+
});
179342

180343
let combinedSheets;
181344

182345
if (importType === 'merge-current-dsheet') {
183-
combinedSheets = [...localSheetsArray, ...sheets]
346+
combinedSheets = [...localSheetsArray, ...sheets];
184347
} else {
185-
combinedSheets = [...sheets]
348+
combinedSheets = [...sheets];
186349
}
187350

188351
combinedSheets = combinedSheets.map((sheet, index) => {
189352
sheet.order = index;
190-
return sheet
191-
})
353+
return sheet;
354+
});
192355

193356
setSheetData(combinedSheets);
194357
ydocRef.current.transact(() => {

package/utils/csv-import.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Sheet } from '@fileverse-dev/fortune-react';
44
import React from 'react';
55
import * as Y from 'yjs';
66
import { WorkbookInstance } from '@fileverse-dev/fortune-react';
7-
import { encode } from 'punycode';
87

98
export const handleCSVUpload = (
109
event: React.ChangeEventHandler<HTMLInputElement> | undefined,
@@ -15,7 +14,7 @@ export const handleCSVUpload = (
1514
sheetEditorRef: React.RefObject<WorkbookInstance | null>,
1615
updateDocumentTitle?: (title: string) => void,
1716
fileArg?: File,
18-
importType?: string
17+
importType?: string,
1918
) => {
2019
const input = event?.target;
2120
if (!input?.files?.length && !fileArg) {
@@ -85,33 +84,41 @@ export const handleCSVUpload = (
8584
// Add data rows
8685
let maxRow = 0;
8786
let maxCol = 0;
87+
const urlRegex = /^https?:\/\/\S+$/i;
88+
const hyperlinkMap: Record<
89+
string,
90+
{ linkType: string; linkAddress: string }
91+
> = {};
8892
results.data.forEach((row, rowIndex) => {
8993
headers.forEach((header, colIndex) => {
94+
const cellValue = (row as Record<string, string | number | null>)[
95+
header
96+
];
97+
const cellStr =
98+
cellValue !== null && cellValue !== undefined
99+
? cellValue.toString()
100+
: null;
101+
const isUrl = cellStr && urlRegex.test(cellStr);
90102
cellData.push({
91103
r: rowIndex + 1, // +1 because header is row 0
92104
c: colIndex,
93105
v: {
94-
// @ts-expect-error later
95-
m:
96-
(row as Record<string, string | null>)[header] !== null
97-
? (row as Record<string, string | null>)[
98-
header
99-
]?.toString()
100-
: null,
106+
m: cellStr,
101107
ct: {
102108
fa: 'General',
103109
t: 'g',
104110
},
105-
// @ts-expect-error later
106-
v:
107-
(row as Record<string, string | number | null>)[header] !==
108-
null
109-
? (row as Record<string, string | number | null>)[
110-
header
111-
]?.toString()
112-
: null,
111+
v: cellStr,
112+
...(isUrl && { fc: 'rgb(0, 0, 255)', un: 1 }),
113113
},
114114
});
115+
// Detect URLs and store as hyperlinks
116+
if (isUrl) {
117+
hyperlinkMap[`${rowIndex + 1}_${colIndex}`] = {
118+
linkType: 'webpage',
119+
linkAddress: cellStr,
120+
};
121+
}
115122
maxRow = Math.max(maxRow, rowIndex + 1);
116123
maxCol = Math.max(maxCol, colIndex + 1);
117124
});
@@ -139,6 +146,9 @@ export const handleCSVUpload = (
139146
config: {
140147
merge: {}, // No merge cells for CSV by default
141148
},
149+
...(Object.keys(hyperlinkMap).length > 0 && {
150+
hyperlink: hyperlinkMap,
151+
}),
142152
};
143153

144154
updateDocumentTitle?.(file.name);

0 commit comments

Comments
 (0)