Skip to content

Commit dc8accc

Browse files
authored
Merge pull request #448 from mfts/feat/excel-sheets
feat: add support for multiple sheets in excel files
2 parents 087872d + 6ef3ed2 commit dc8accc

File tree

9 files changed

+132
-62
lines changed

9 files changed

+132
-62
lines changed

components/view/dataroom/dataroom-view.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ const ExcelViewer = dynamic(
2727
{ ssr: false },
2828
);
2929

30+
type RowData = { [key: string]: any };
31+
type SheetData = {
32+
sheetName: string;
33+
columnData: string[];
34+
rowData: RowData[];
35+
};
36+
3037
export type TDocumentData = {
3138
id: string;
3239
name: string;
@@ -44,10 +51,7 @@ export type DEFAULT_DOCUMENT_VIEW_TYPE = {
4451
pages?:
4552
| { file: string; pageNumber: string; embeddedLinks: string[] }[]
4653
| null;
47-
sheetData?: {
48-
rowData: { [key: string]: any }[];
49-
columnData: string[];
50-
} | null;
54+
sheetData?: SheetData[] | null;
5155
notionData?: { recordMap: ExtendedRecordMap | null };
5256
};
5357

@@ -268,8 +272,7 @@ export default function DataroomView({
268272
documentId={documentData.id}
269273
documentName={documentData.name}
270274
versionNumber={documentData.documentVersionNumber}
271-
columns={viewData.sheetData.columnData!}
272-
data={viewData.sheetData.rowData!}
275+
sheetData={viewData.sheetData}
273276
brand={brand}
274277
dataroomId={dataroom.id}
275278
setDocumentData={setDocumentData}

components/view/document-view.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ import { LinkWithDocument } from "@/lib/types";
1919
import EmailVerificationMessage from "./email-verification-form";
2020
import ViewData from "./view-data";
2121

22+
type RowData = { [key: string]: any };
23+
type SheetData = {
24+
sheetName: string;
25+
columnData: string[];
26+
rowData: RowData[];
27+
};
28+
2229
export type DEFAULT_DOCUMENT_VIEW_TYPE = {
2330
viewId: string;
2431
file?: string | null;
2532
pages?:
2633
| { file: string; pageNumber: string; embeddedLinks: string[] }[]
2734
| null;
28-
sheetData?: {
29-
rowData: { [key: string]: any }[];
30-
columnData: string[];
31-
} | null;
35+
sheetData?: SheetData[] | null;
3236
};
3337

3438
export default function DocumentView({

components/view/view-data.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ export default function ViewData({
5050
documentId={document.id}
5151
documentName={document.name}
5252
versionNumber={document.versions[0].versionNumber}
53-
columns={viewData.sheetData.columnData!}
54-
data={viewData.sheetData.rowData!}
53+
sheetData={viewData.sheetData}
5554
brand={brand}
5655
/>
5756
) : viewData.pages ? (

components/view/viewer/excel-viewer.tsx

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,20 @@ import React from "react";
44
import "@/public/vendor/handsontable/handsontable.full.min.css";
55
import { Brand, DataroomBrand } from "@prisma/client";
66

7+
import { Button } from "@/components/ui/button";
8+
9+
import { cn } from "@/lib/utils";
10+
711
import { TDocumentData } from "../dataroom/dataroom-view";
812
import Nav from "../nav";
913

1014
// Define the type for the JSON data
11-
type SheetData = { [key: string]: any };
15+
type RowData = { [key: string]: any };
16+
type SheetData = {
17+
sheetName: string;
18+
columnData: string[];
19+
rowData: RowData[];
20+
};
1221

1322
const trackPageView = async (data: {
1423
linkId: string;
@@ -34,8 +43,7 @@ export default function ExcelViewer({
3443
documentId,
3544
documentName,
3645
versionNumber,
37-
columns,
38-
data,
46+
sheetData,
3947
brand,
4048
dataroomId,
4149
setDocumentData,
@@ -45,15 +53,15 @@ export default function ExcelViewer({
4553
documentId: string;
4654
documentName: string;
4755
versionNumber: number;
48-
columns: string[];
49-
data: SheetData[];
56+
sheetData: SheetData[];
5057
brand?: Partial<Brand> | Partial<DataroomBrand> | null;
5158
dataroomId?: string;
5259
setDocumentData?: React.Dispatch<React.SetStateAction<TDocumentData | null>>;
5360
}) {
5461
const [availableWidth, setAvailableWidth] = useState<number>(200);
5562
const [availableHeight, setAvailableHeight] = useState<number>(200);
5663
const [handsontableLoaded, setHandsontableLoaded] = useState<boolean>(false);
64+
const [selectedSheetIndex, setSelectedSheetIndex] = useState<number>(0);
5765

5866
useEffect(() => {
5967
const script = document.createElement("script");
@@ -84,8 +92,8 @@ export default function ExcelViewer({
8492
const calculateSize = () => {
8593
if (containerRef.current) {
8694
const offset = containerRef.current.getBoundingClientRect();
87-
setAvailableWidth(Math.max(offset.width - 60, 200));
88-
setAvailableHeight(Math.max(offset.height - 10, 200));
95+
setAvailableWidth(Math.max(offset.width, 200));
96+
setAvailableHeight(Math.max(offset.height - 50, 200));
8997
}
9098
};
9199

@@ -113,7 +121,7 @@ export default function ExcelViewer({
113121
documentId,
114122
viewId,
115123
duration,
116-
pageNumber: 1,
124+
pageNumber: selectedSheetIndex + 1,
117125
versionNumber,
118126
dataroomId,
119127
});
@@ -130,24 +138,27 @@ export default function ExcelViewer({
130138
documentId,
131139
viewId,
132140
duration,
133-
pageNumber: 1,
141+
pageNumber: selectedSheetIndex + 1,
134142
versionNumber,
135143
dataroomId,
136144
}); // Also capture duration if component unmounts while visible
145+
startTimeRef.current = Date.now();
137146
}
138147
document.removeEventListener("visibilitychange", handleVisibilityChange);
139148
};
140-
}, []);
149+
}, [selectedSheetIndex]);
141150

142151
useEffect(() => {
143152
const handleBeforeUnload = () => {
153+
if (!visibilityRef.current) return;
154+
144155
const duration = Date.now() - startTimeRef.current;
145156
trackPageView({
146157
linkId,
147158
documentId,
148159
viewId,
149160
duration,
150-
pageNumber: 1,
161+
pageNumber: selectedSheetIndex + 1,
151162
versionNumber,
152163
dataroomId,
153164
});
@@ -158,22 +169,24 @@ export default function ExcelViewer({
158169
return () => {
159170
window.removeEventListener("beforeunload", handleBeforeUnload);
160171
};
161-
}, []);
172+
}, [selectedSheetIndex]);
162173

163174
useEffect(() => {
164-
if (handsontableLoaded && data.length && columns.length) {
175+
if (handsontableLoaded && sheetData.length) {
165176
if (hotInstanceRef.current) {
166177
hotInstanceRef.current.destroy();
167178
}
168179

180+
const { columnData, rowData } = sheetData[selectedSheetIndex];
181+
169182
// @ts-ignore - Handsontable import has not types
170183
hotInstanceRef.current = new Handsontable(hotRef.current!, {
171-
data: data,
184+
data: rowData,
172185
readOnly: true,
173186
disableVisualSelection: true,
174187
comments: false,
175188
contextMenu: false,
176-
colHeaders: columns,
189+
colHeaders: columnData,
177190
rowHeaders: true,
178191
manualColumnResize: true,
179192
width: availableWidth,
@@ -190,7 +203,13 @@ export default function ExcelViewer({
190203
// },
191204
});
192205
}
193-
}, [handsontableLoaded, data, columns, availableHeight, availableWidth]);
206+
}, [
207+
handsontableLoaded,
208+
sheetData,
209+
selectedSheetIndex,
210+
availableHeight,
211+
availableWidth,
212+
]);
194213

195214
return (
196215
<>
@@ -202,11 +221,27 @@ export default function ExcelViewer({
202221
type="sheet"
203222
/>
204223
<div
205-
style={{ height: "calc(100vh - 64px)" }}
206-
className="flex h-screen items-center justify-center"
224+
style={{ height: "calc(100dvh - 64px)" }}
225+
className="mx-2 flex h-screen flex-col sm:mx-6 lg:mx-8"
207226
ref={containerRef}
208227
>
209-
<div ref={hotRef}></div>
228+
<div className="" ref={hotRef}></div>
229+
<div className="flex max-w-fit divide-x divide-gray-200 overflow-x-scroll whitespace-nowrap rounded-b-sm bg-[#f0f0f0] px-1 ">
230+
{sheetData.map((sheet, index) => (
231+
<div className="px-1" key={sheet.sheetName}>
232+
<Button
233+
onClick={() => setSelectedSheetIndex(index)}
234+
className={cn(
235+
"mb-1 rounded-none rounded-b-sm bg-[#f0f0f0] font-normal text-gray-950 hover:bg-gray-50",
236+
index === selectedSheetIndex &&
237+
"bg-white font-medium text-black ring-1 ring-gray-500 hover:bg-white",
238+
)}
239+
>
240+
{sheet.sheetName}
241+
</Button>
242+
</div>
243+
))}
244+
</div>
210245
</div>
211246
</>
212247
);

lib/files/put-file.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { upload } from "@vercel/blob/client";
33
import { match } from "ts-pattern";
44

55
import { newId } from "@/lib/id-helper";
6-
import { getPagesCount } from "@/lib/utils/get-page-number-count";
6+
import {
7+
getPagesCount,
8+
getSheetsCount,
9+
} from "@/lib/utils/get-page-number-count";
710

811
import { SUPPORTED_DOCUMENT_TYPES } from "../constants";
912

@@ -116,10 +119,19 @@ const putFileInS3 = async ({
116119
}
117120

118121
let numPages: number = 1;
122+
// get page count for pdf files
119123
if (file.type === "application/pdf") {
120124
const body = await file.arrayBuffer();
121125
numPages = await getPagesCount(body);
122126
}
127+
// get sheet count for excel files
128+
else if (
129+
SUPPORTED_DOCUMENT_TYPES.includes(file.type) &&
130+
file.type !== "application/pdf"
131+
) {
132+
const body = await file.arrayBuffer();
133+
numPages = getSheetsCount(body);
134+
}
123135

124136
return {
125137
type: DocumentStorageType.S3_PATH,

lib/sheet/index.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as XLSX from "xlsx";
22

33
type RowData = { [key: string]: any };
4+
type SheetData = {
5+
sheetName: string;
6+
columnData: string[];
7+
rowData: RowData[];
8+
};
49

510
// Custom sort function to sort keys A, B, .. Z, AA, AB, ..
611
const customSort = (a: string, b: string) => {
@@ -11,37 +16,42 @@ const customSort = (a: string, b: string) => {
1116
};
1217

1318
export const parseSheet = async ({ fileUrl }: { fileUrl: string }) => {
14-
let columnData: string[] | null = null;
15-
let rowData: RowData[] | null = null;
16-
1719
const response = await fetch(fileUrl);
1820
const arrayBuffer = await response.arrayBuffer();
1921
const data = new Uint8Array(arrayBuffer);
2022
const workbook = XLSX.read(data, { type: "array" });
21-
const firstSheetName = workbook.SheetNames[0];
22-
const worksheet = workbook.Sheets[firstSheetName];
2323

24-
const json: RowData[] = XLSX.utils.sheet_to_json(worksheet, {
25-
header: "A",
26-
});
24+
const result: SheetData[] = [];
25+
26+
// Iterate through all sheets in the workbook
27+
workbook.SheetNames.forEach((sheetName) => {
28+
const worksheet = workbook.Sheets[sheetName];
29+
const json: RowData[] = XLSX.utils.sheet_to_json(worksheet, {
30+
header: "A",
31+
});
2732

28-
// Collect all unique keys from the JSON data
29-
const allKeys = Array.from(new Set(json.flatMap(Object.keys)));
33+
// Collect all unique keys from the JSON data
34+
const allKeys = Array.from(new Set(json.flatMap(Object.keys)));
3035

31-
// Sort the keys alphabetically
32-
allKeys.sort(customSort);
36+
// Sort the keys alphabetically
37+
allKeys.sort(customSort);
3338

34-
// Ensure each row has the same set of keys
35-
const normalizedData = json.map((row) => {
36-
const normalizedRow: RowData = {};
37-
allKeys.forEach((key) => {
38-
normalizedRow[key] = row[key] || "";
39+
// Ensure each row has the same set of keys
40+
const normalizedData = json.map((row) => {
41+
const normalizedRow: RowData = {};
42+
allKeys.forEach((key) => {
43+
normalizedRow[key] = row[key] || "";
44+
});
45+
return normalizedRow;
3946
});
40-
return normalizedRow;
41-
});
4247

43-
columnData = allKeys;
44-
rowData = normalizedData;
48+
// Store column and row data for the current sheet
49+
result.push({
50+
sheetName,
51+
columnData: allKeys,
52+
rowData: normalizedData,
53+
});
54+
});
4555

46-
return { columnData, rowData };
56+
return result;
4757
};

lib/utils/get-page-number-count.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { pdfjs } from "react-pdf";
2+
import * as XLSX from "xlsx";
23

34
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
45

56
export const getPagesCount = async (arrayBuffer: ArrayBuffer) => {
67
const pdf = await pdfjs.getDocument(arrayBuffer).promise;
78
return pdf.numPages;
89
};
10+
11+
export const getSheetsCount = (arrayBuffer: ArrayBuffer) => {
12+
const data = new Uint8Array(arrayBuffer);
13+
const workbook = XLSX.read(data, { type: "array" });
14+
return workbook.SheetNames.length ?? 1;
15+
};

pages/api/views-dataroom.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ export default async function handle(
311311
// otherwise, return file from document version
312312
let documentPages, documentVersion;
313313
let recordMap;
314-
let columnData, rowData;
314+
let sheetData;
315315

316316
if (hasPages) {
317317
// get pages from document version
@@ -379,8 +379,7 @@ export default async function handle(
379379
});
380380

381381
const data = await parseSheet({ fileUrl });
382-
columnData = data.columnData;
383-
rowData = data.rowData;
382+
sheetData = data;
384383
}
385384
console.timeEnd("get-file");
386385
}
@@ -396,7 +395,7 @@ export default async function handle(
396395
notionData: recordMap ? { recordMap } : undefined,
397396
sheetData:
398397
documentVersion && documentVersion.type === "sheet"
399-
? { columnData, rowData }
398+
? sheetData
400399
: undefined,
401400
};
402401

0 commit comments

Comments
 (0)