From 1852831681bd4ac8b6f9dd2be5116f067e447e2e Mon Sep 17 00:00:00 2001 From: Marc Seitz Date: Fri, 31 May 2024 16:25:27 +0200 Subject: [PATCH 1/3] feat: make excel multi-sheet possible --- components/view/dataroom/dataroom-view.tsx | 15 +++-- components/view/document-view.tsx | 12 ++-- components/view/view-data.tsx | 3 +- components/view/viewer/excel-viewer.tsx | 74 ++++++++++++++++------ lib/sheet/index.ts | 51 +++++++++------ pages/api/views-dataroom.ts | 7 +- pages/api/views.ts | 9 +-- 7 files changed, 113 insertions(+), 58 deletions(-) diff --git a/components/view/dataroom/dataroom-view.tsx b/components/view/dataroom/dataroom-view.tsx index 7d210a9c6..1863bd986 100644 --- a/components/view/dataroom/dataroom-view.tsx +++ b/components/view/dataroom/dataroom-view.tsx @@ -27,6 +27,13 @@ const ExcelViewer = dynamic( { ssr: false }, ); +type RowData = { [key: string]: any }; +type SheetData = { + sheetName: string; + columnData: string[]; + rowData: RowData[]; +}; + export type TDocumentData = { id: string; name: string; @@ -44,10 +51,7 @@ export type DEFAULT_DOCUMENT_VIEW_TYPE = { pages?: | { file: string; pageNumber: string; embeddedLinks: string[] }[] | null; - sheetData?: { - rowData: { [key: string]: any }[]; - columnData: string[]; - } | null; + sheetData?: SheetData[] | null; notionData?: { recordMap: ExtendedRecordMap | null }; }; @@ -268,8 +272,7 @@ export default function DataroomView({ documentId={documentData.id} documentName={documentData.name} versionNumber={documentData.documentVersionNumber} - columns={viewData.sheetData.columnData!} - data={viewData.sheetData.rowData!} + sheetData={viewData.sheetData} brand={brand} dataroomId={dataroom.id} setDocumentData={setDocumentData} diff --git a/components/view/document-view.tsx b/components/view/document-view.tsx index 540da0662..3a0c76011 100644 --- a/components/view/document-view.tsx +++ b/components/view/document-view.tsx @@ -19,16 +19,20 @@ import { LinkWithDocument } from "@/lib/types"; import EmailVerificationMessage from "./email-verification-form"; import ViewData from "./view-data"; +type RowData = { [key: string]: any }; +type SheetData = { + sheetName: string; + columnData: string[]; + rowData: RowData[]; +}; + export type DEFAULT_DOCUMENT_VIEW_TYPE = { viewId: string; file?: string | null; pages?: | { file: string; pageNumber: string; embeddedLinks: string[] }[] | null; - sheetData?: { - rowData: { [key: string]: any }[]; - columnData: string[]; - } | null; + sheetData?: SheetData[] | null; }; export default function DocumentView({ diff --git a/components/view/view-data.tsx b/components/view/view-data.tsx index 848b019f1..ab8c7c1bb 100644 --- a/components/view/view-data.tsx +++ b/components/view/view-data.tsx @@ -50,8 +50,7 @@ export default function ViewData({ documentId={document.id} documentName={document.name} versionNumber={document.versions[0].versionNumber} - columns={viewData.sheetData.columnData!} - data={viewData.sheetData.rowData!} + sheetData={viewData.sheetData} brand={brand} /> ) : viewData.pages ? ( diff --git a/components/view/viewer/excel-viewer.tsx b/components/view/viewer/excel-viewer.tsx index a5e458856..0abafa809 100644 --- a/components/view/viewer/excel-viewer.tsx +++ b/components/view/viewer/excel-viewer.tsx @@ -4,11 +4,20 @@ import React from "react"; import "@/public/vendor/handsontable/handsontable.full.min.css"; import { Brand, DataroomBrand } from "@prisma/client"; +import { Button } from "@/components/ui/button"; + +import { cn } from "@/lib/utils"; + import { TDocumentData } from "../dataroom/dataroom-view"; import Nav from "../nav"; // Define the type for the JSON data -type SheetData = { [key: string]: any }; +type RowData = { [key: string]: any }; +type SheetData = { + sheetName: string; + columnData: string[]; + rowData: RowData[]; +}; const trackPageView = async (data: { linkId: string; @@ -34,8 +43,7 @@ export default function ExcelViewer({ documentId, documentName, versionNumber, - columns, - data, + sheetData, brand, dataroomId, setDocumentData, @@ -45,8 +53,7 @@ export default function ExcelViewer({ documentId: string; documentName: string; versionNumber: number; - columns: string[]; - data: SheetData[]; + sheetData: SheetData[]; brand?: Partial | Partial | null; dataroomId?: string; setDocumentData?: React.Dispatch>; @@ -54,6 +61,7 @@ export default function ExcelViewer({ const [availableWidth, setAvailableWidth] = useState(200); const [availableHeight, setAvailableHeight] = useState(200); const [handsontableLoaded, setHandsontableLoaded] = useState(false); + const [selectedSheetIndex, setSelectedSheetIndex] = useState(0); useEffect(() => { const script = document.createElement("script"); @@ -84,8 +92,8 @@ export default function ExcelViewer({ const calculateSize = () => { if (containerRef.current) { const offset = containerRef.current.getBoundingClientRect(); - setAvailableWidth(Math.max(offset.width - 60, 200)); - setAvailableHeight(Math.max(offset.height - 10, 200)); + setAvailableWidth(Math.max(offset.width, 200)); + setAvailableHeight(Math.max(offset.height - 50, 200)); } }; @@ -113,7 +121,7 @@ export default function ExcelViewer({ documentId, viewId, duration, - pageNumber: 1, + pageNumber: selectedSheetIndex + 1, versionNumber, dataroomId, }); @@ -130,24 +138,27 @@ export default function ExcelViewer({ documentId, viewId, duration, - pageNumber: 1, + pageNumber: selectedSheetIndex + 1, versionNumber, dataroomId, }); // Also capture duration if component unmounts while visible + startTimeRef.current = Date.now(); } document.removeEventListener("visibilitychange", handleVisibilityChange); }; - }, []); + }, [selectedSheetIndex]); useEffect(() => { const handleBeforeUnload = () => { + if (!visibilityRef.current) return; + const duration = Date.now() - startTimeRef.current; trackPageView({ linkId, documentId, viewId, duration, - pageNumber: 1, + pageNumber: selectedSheetIndex + 1, versionNumber, dataroomId, }); @@ -158,22 +169,24 @@ export default function ExcelViewer({ return () => { window.removeEventListener("beforeunload", handleBeforeUnload); }; - }, []); + }, [selectedSheetIndex]); useEffect(() => { - if (handsontableLoaded && data.length && columns.length) { + if (handsontableLoaded && sheetData.length) { if (hotInstanceRef.current) { hotInstanceRef.current.destroy(); } + const { columnData, rowData } = sheetData[selectedSheetIndex]; + // @ts-ignore - Handsontable import has not types hotInstanceRef.current = new Handsontable(hotRef.current!, { - data: data, + data: rowData, readOnly: true, disableVisualSelection: true, comments: false, contextMenu: false, - colHeaders: columns, + colHeaders: columnData, rowHeaders: true, manualColumnResize: true, width: availableWidth, @@ -190,7 +203,13 @@ export default function ExcelViewer({ // }, }); } - }, [handsontableLoaded, data, columns, availableHeight, availableWidth]); + }, [ + handsontableLoaded, + sheetData, + selectedSheetIndex, + availableHeight, + availableWidth, + ]); return ( <> @@ -202,11 +221,28 @@ export default function ExcelViewer({ type="sheet" />
-
+
+
+ {sheetData.map((sheet, index) => ( +
+ +
+ ))} +
); diff --git a/lib/sheet/index.ts b/lib/sheet/index.ts index 8048210a7..134c63517 100644 --- a/lib/sheet/index.ts +++ b/lib/sheet/index.ts @@ -1,6 +1,11 @@ import * as XLSX from "xlsx"; type RowData = { [key: string]: any }; +type SheetData = { + sheetName: string; + columnData: string[]; + rowData: RowData[]; +}; // Custom sort function to sort keys A, B, .. Z, AA, AB, .. const customSort = (a: string, b: string) => { @@ -18,30 +23,38 @@ export const parseSheet = async ({ fileUrl }: { fileUrl: string }) => { const arrayBuffer = await response.arrayBuffer(); const data = new Uint8Array(arrayBuffer); const workbook = XLSX.read(data, { type: "array" }); - const firstSheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheetName]; - const json: RowData[] = XLSX.utils.sheet_to_json(worksheet, { - header: "A", - }); + const result: SheetData[] = []; + + // Iterate through all sheets in the workbook + workbook.SheetNames.forEach((sheetName) => { + const worksheet = workbook.Sheets[sheetName]; + const json: RowData[] = XLSX.utils.sheet_to_json(worksheet, { + header: "A", + }); - // Collect all unique keys from the JSON data - const allKeys = Array.from(new Set(json.flatMap(Object.keys))); + // Collect all unique keys from the JSON data + const allKeys = Array.from(new Set(json.flatMap(Object.keys))); - // Sort the keys alphabetically - allKeys.sort(customSort); + // Sort the keys alphabetically + allKeys.sort(customSort); - // Ensure each row has the same set of keys - const normalizedData = json.map((row) => { - const normalizedRow: RowData = {}; - allKeys.forEach((key) => { - normalizedRow[key] = row[key] || ""; + // Ensure each row has the same set of keys + const normalizedData = json.map((row) => { + const normalizedRow: RowData = {}; + allKeys.forEach((key) => { + normalizedRow[key] = row[key] || ""; + }); + return normalizedRow; }); - return normalizedRow; - }); - columnData = allKeys; - rowData = normalizedData; + // Store column and row data for the current sheet + result.push({ + sheetName, + columnData: allKeys, + rowData: normalizedData, + }); + }); - return { columnData, rowData }; + return result; }; diff --git a/pages/api/views-dataroom.ts b/pages/api/views-dataroom.ts index afaea11a0..577063e48 100644 --- a/pages/api/views-dataroom.ts +++ b/pages/api/views-dataroom.ts @@ -311,7 +311,7 @@ export default async function handle( // otherwise, return file from document version let documentPages, documentVersion; let recordMap; - let columnData, rowData; + let sheetData; if (hasPages) { // get pages from document version @@ -379,8 +379,7 @@ export default async function handle( }); const data = await parseSheet({ fileUrl }); - columnData = data.columnData; - rowData = data.rowData; + sheetData = data; } console.timeEnd("get-file"); } @@ -396,7 +395,7 @@ export default async function handle( notionData: recordMap ? { recordMap } : undefined, sheetData: documentVersion && documentVersion.type === "sheet" - ? { columnData, rowData } + ? sheetData : undefined, }; diff --git a/pages/api/views.ts b/pages/api/views.ts index 0d7bc2138..0e8bc5f96 100644 --- a/pages/api/views.ts +++ b/pages/api/views.ts @@ -221,7 +221,7 @@ export default async function handle( // if document version has pages, then return pages // otherwise, return file from document version let documentPages, documentVersion; - let columnData, rowData; + let sheetData; // let documentPagesPromise, documentVersionPromise; if (hasPages) { // get pages from document version @@ -279,8 +279,9 @@ export default async function handle( }); const data = await parseSheet({ fileUrl }); - columnData = data.columnData; - rowData = data.rowData; + sheetData = data; + // columnData = data.columnData; + // rowData = data.rowData; } console.timeEnd("get-file"); } @@ -301,7 +302,7 @@ export default async function handle( pages: documentPages ? documentPages : undefined, sheetData: documentVersion && documentVersion.type === "sheet" - ? { columnData, rowData } + ? sheetData : undefined, }; From faa5cd03e474d52ede0499316e71c33f79a03d20 Mon Sep 17 00:00:00 2001 From: Marc Seitz Date: Fri, 31 May 2024 16:42:39 +0200 Subject: [PATCH 2/3] feat: get accurate sheet count for uploaded documents --- lib/files/put-file.ts | 14 +++++++++++++- lib/sheet/index.ts | 3 --- lib/utils/get-page-number-count.ts | 7 +++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/files/put-file.ts b/lib/files/put-file.ts index 6ddc4e085..ed38eaa32 100644 --- a/lib/files/put-file.ts +++ b/lib/files/put-file.ts @@ -3,7 +3,10 @@ import { upload } from "@vercel/blob/client"; import { match } from "ts-pattern"; import { newId } from "@/lib/id-helper"; -import { getPagesCount } from "@/lib/utils/get-page-number-count"; +import { + getPagesCount, + getSheetsCount, +} from "@/lib/utils/get-page-number-count"; import { SUPPORTED_DOCUMENT_TYPES } from "../constants"; @@ -116,10 +119,19 @@ const putFileInS3 = async ({ } let numPages: number = 1; + // get page count for pdf files if (file.type === "application/pdf") { const body = await file.arrayBuffer(); numPages = await getPagesCount(body); } + // get sheet count for excel files + else if ( + SUPPORTED_DOCUMENT_TYPES.includes(file.type) && + file.type !== "application/pdf" + ) { + const body = await file.arrayBuffer(); + numPages = getSheetsCount(body); + } return { type: DocumentStorageType.S3_PATH, diff --git a/lib/sheet/index.ts b/lib/sheet/index.ts index 134c63517..a03d378b0 100644 --- a/lib/sheet/index.ts +++ b/lib/sheet/index.ts @@ -16,9 +16,6 @@ const customSort = (a: string, b: string) => { }; export const parseSheet = async ({ fileUrl }: { fileUrl: string }) => { - let columnData: string[] | null = null; - let rowData: RowData[] | null = null; - const response = await fetch(fileUrl); const arrayBuffer = await response.arrayBuffer(); const data = new Uint8Array(arrayBuffer); diff --git a/lib/utils/get-page-number-count.ts b/lib/utils/get-page-number-count.ts index 1922bb9e3..0387f56c3 100644 --- a/lib/utils/get-page-number-count.ts +++ b/lib/utils/get-page-number-count.ts @@ -1,4 +1,5 @@ import { pdfjs } from "react-pdf"; +import * as XLSX from "xlsx"; pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`; @@ -6,3 +7,9 @@ export const getPagesCount = async (arrayBuffer: ArrayBuffer) => { const pdf = await pdfjs.getDocument(arrayBuffer).promise; return pdf.numPages; }; + +export const getSheetsCount = (arrayBuffer: ArrayBuffer) => { + const data = new Uint8Array(arrayBuffer); + const workbook = XLSX.read(data, { type: "array" }); + return workbook.SheetNames.length ?? 1; +}; From 6ef3ed2e80c275dae030dc3fc3e2bbffe38ad640 Mon Sep 17 00:00:00 2001 From: Marc Seitz Date: Fri, 31 May 2024 16:45:32 +0200 Subject: [PATCH 3/3] fix --- components/view/viewer/excel-viewer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/view/viewer/excel-viewer.tsx b/components/view/viewer/excel-viewer.tsx index 0abafa809..5535b1279 100644 --- a/components/view/viewer/excel-viewer.tsx +++ b/components/view/viewer/excel-viewer.tsx @@ -228,9 +228,8 @@ export default function ExcelViewer({
{sheetData.map((sheet, index) => ( -
+