diff --git a/blocks/sheet.tsx b/blocks/sheet.tsx index 379a757da..33d84cf43 100644 --- a/blocks/sheet.tsx +++ b/blocks/sheet.tsx @@ -1,5 +1,6 @@ import { Block } from '@/components/create-block'; import { + DownloadIcon, MessageIcon, RedoIcon, SparklesIcon, @@ -72,7 +73,7 @@ export const sheetBlock = new Block<'sheet', Metadata>({ }, }, { - icon: , + icon: , description: 'Export', onClick: ({ content }) => { try { diff --git a/components/icons.tsx b/components/icons.tsx index c67f23961..802bc6e02 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -1119,20 +1119,20 @@ export const FullscreenIcon = ({ size = 16 }: { size?: number }) => ( > ); + export const DownloadIcon = ({ size = 16 }: { size?: number }) => ( - - - + ); diff --git a/components/sheet-editor.tsx b/components/sheet-editor.tsx index a68c3e01b..ce5551d21 100644 --- a/components/sheet-editor.tsx +++ b/components/sheet-editor.tsx @@ -1,21 +1,11 @@ 'use client'; -import React, { memo } from 'react'; -import 'handsontable/styles/handsontable.min.css'; -import 'handsontable/styles/ht-theme-main.min.css'; -import { registerAllModules } from 'handsontable/registry'; +import React, { memo, useMemo } from 'react'; +import DataGrid from 'react-data-grid'; +import 'react-data-grid/lib/styles.css'; import { parse, unparse } from 'papaparse'; -registerAllModules(); - -import { HotTable } from '@handsontable/react-wrapper'; - -interface SpreadsheetData { - headers: string[]; - rows: string[][]; -} - -interface SpreadsheetEditorProps { +interface SheetEditorProps { content: string; saveContent: (updatedContent: string, debounce: boolean) => void; status: 'streaming' | 'idle'; @@ -28,53 +18,91 @@ const PureSpreadsheetEditor = ({ saveContent, status, isCurrentVersion, -}: SpreadsheetEditorProps) => { - const parseData = (csvContent: string): string[][] | null => { +}: SheetEditorProps) => { + const parseData = (csvContent: string) => { if (!csvContent) return null; - const result = parse(csvContent, { skipEmptyLines: true }); return result.data; }; - const generateCsv = (data: string[][]) => { + const generateCsv = (data: any[][]) => { return unparse(data); }; - const data = parseData(content); - - return data ? ( - { - if (changes && isCurrentVersion) { - const newData: string[][] = data.map((row) => [...row]); - - changes.forEach(([row, prop, _, newValue]) => { - if (typeof row === 'number' && typeof prop === 'number') { - newData[row][prop] = String(newValue); - } - }); - - const updatedCsv = generateCsv(newData); - saveContent(updatedCsv, true); - } - }} - /> + const rawData = parseData(content); + + const columns = useMemo(() => { + if (!rawData || rawData.length === 0) return []; + + const columnCount = Math.max(...rawData.map((row) => row.length)); + return Array.from({ length: columnCount }, (_, i) => ({ + key: i.toString(), + name: String.fromCharCode(65 + i), + editor: 'textEditor', + editable: true, + })); + }, [rawData]); + + const rows = useMemo(() => { + if (!rawData) return []; + + return rawData.map((row, rowIndex) => { + const rowData: any = { id: rowIndex }; + + columns.forEach((col, colIndex) => { + rowData[col.key] = row[colIndex] || ''; + }); + + return rowData; + }); + }, [rawData, columns]); + + function onCellEdit(rowIndex: number, columnKey: string, newValue: string) { + if (!isCurrentVersion) return; + + const newRows = [...rows]; + newRows[rowIndex] = { ...newRows[rowIndex], [columnKey]: newValue }; + + // Convert the rows back to 2D array format + const newData = newRows.map((row) => + columns.map((col) => row[col.key] || ''), + ); + + const updatedCsv = generateCsv(newData); + saveContent(updatedCsv, true); + } + + return rawData ? ( +
+ { + args.selectCell(); + }} + onCellKeyDown={(args) => { + if (args.mode !== 'EDIT' && args.row.id !== undefined) { + args.enableEditMode(); + } + }} + onRowsChange={(newRows, { indexes, column }) => { + if (indexes.length === 1) { + const rowIndex = indexes[0]; + const newValue = newRows[rowIndex][column.key]; + onCellEdit(rowIndex, column.key, newValue); + } + }} + style={{ height: '100%' }} + defaultColumnOptions={{ + resizable: true, + sortable: true, + }} + /> +
) : null; }; -function areEqual( - prevProps: SpreadsheetEditorProps, - nextProps: SpreadsheetEditorProps, -) { +function areEqual(prevProps: SheetEditorProps, nextProps: SheetEditorProps) { return ( prevProps.currentVersionIndex === nextProps.currentVersionIndex && prevProps.isCurrentVersion === nextProps.isCurrentVersion && diff --git a/package.json b/package.json index 967a5cc1c..dece591e9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@codemirror/state": "^6.5.0", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.35.3", - "@handsontable/react-wrapper": "^15.0.0", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -51,7 +50,6 @@ "fast-deep-equal": "^3.1.3", "framer-motion": "^11.3.19", "geist": "^1.3.1", - "handsontable": "^15.0.0", "lucide-react": "^0.446.0", "nanoid": "^5.0.8", "next": "15.0.3-canary.2", @@ -69,6 +67,7 @@ "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.34.3", "react": "19.0.0-rc-45804af1-20241021", + "react-data-grid": "7.0.0-beta.47", "react-dom": "19.0.0-rc-45804af1-20241021", "react-markdown": "^9.0.1", "react-resizable-panels": "^2.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0932612fd..11d5d0482 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: '@codemirror/view': specifier: ^6.35.3 version: 6.35.3 - '@handsontable/react-wrapper': - specifier: ^15.0.0 - version: 15.0.0(handsontable@15.0.0) '@radix-ui/react-alert-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) @@ -107,9 +104,6 @@ importers: geist: specifier: ^1.3.1 version: 1.3.1(next@15.0.3-canary.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)) - handsontable: - specifier: ^15.0.0 - version: 15.0.0 lucide-react: specifier: ^0.446.0 version: 0.446.0(react@19.0.0-rc-45804af1-20241021) @@ -161,6 +155,9 @@ importers: react: specifier: 19.0.0-rc-45804af1-20241021 version: 19.0.0-rc-45804af1-20241021 + react-data-grid: + specifier: 7.0.0-beta.47 + version: 7.0.0-beta.47(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) react-dom: specifier: 19.0.0-rc-45804af1-20241021 version: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) @@ -876,14 +873,6 @@ packages: '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} - '@handsontable/pikaday@1.0.0': - resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==} - - '@handsontable/react-wrapper@15.0.0': - resolution: {integrity: sha512-78uCgcVBM5JjIVYb9stShbKQ126RCEnpjojYMNVF+EqIhc0KzvlKoVKonDm6k6PoQz2b1aMkHL17BEBxumsU/w==} - peerDependencies: - handsontable: '>=15.0.0' - '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1580,9 +1569,6 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1768,9 +1754,6 @@ packages: resolution: {integrity: sha512-gDwQ5784AkkfhHACh3jGcg1hUubyZyeq9AtVd5gXkcyHGVOC+mORjRIHSj+fHfqwY5vxwyBLXQpcfk8MpK0ROg==} engines: {node: '>=18'} - bignumber.js@9.1.2: - resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1838,9 +1821,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chevrotain@6.5.0: - resolution: {integrity: sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1893,9 +1873,6 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} - core-js@3.40.0: - resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -1995,9 +1972,6 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dompurify@3.2.3: - resolution: {integrity: sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==} - dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -2437,9 +2411,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handsontable@15.0.0: - resolution: {integrity: sha512-vv3d8m9tmXZDE/+zzyMyMmFu93LxcYCsm0DN+nPaEPzNMk9+5B29o6K6vc6nPJhzB1k/sPu3J393UMQYf3/Rgw==} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2475,9 +2446,6 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - hyperformula@2.7.1: - resolution: {integrity: sha512-mpVF5zOyNpksZzgTaCQyRAzdC/X43+taz5x1n7zNbs/PUUv0AuLmsy2yfihCr+vihUzN/pk+gXBbOfNpXKOpgA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2922,9 +2890,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3000,9 +2965,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - numbro@2.5.0: - resolution: {integrity: sha512-xDcctDimhzko/e+y+Q2/8i3qNC9Svw1QgOkSkQoO0kIPI473tR9QRbo2KP88Ty9p8WbPy+3OpTaAIzehtuHq+A==} - oauth4webapi@3.1.2: resolution: {integrity: sha512-KQZkNU+xn02lWrFu5Vjqg9E81yPtDSxUZorRHlLWVoojD+H/0GFbH59kcnz5Thdjj7c4/mYMBPj/mhvGe/kKXA==} @@ -3282,6 +3244,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-data-grid@7.0.0-beta.47: + resolution: {integrity: sha512-28kjsmwQGD/9RXYC50zn5Zv/SQMhBBoSvG5seq0fM8XXi9TZ0zr9Z5T3YJqLwcEtoNzTOq3y0njkmdujGkIwQQ==} + peerDependencies: + react: ^18.0 || ^19.0 + react-dom: ^18.0 || ^19.0 + react-dom@19.0.0-rc-45804af1-20241021: resolution: {integrity: sha512-8hOckEFO7Vxo+nH/EEddIGdencOFT0/3iJqF3mKrqv71n1xxhYcp0595JbT/DP31G8bHfDcBSMWVhIvyCGWy/A==} peerDependencies: @@ -3347,9 +3315,6 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} - regexp-to-ast@0.4.0: - resolution: {integrity: sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==} - regexp.prototype.flags@1.5.3: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} @@ -3602,9 +3567,6 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - tiny-emitter@2.1.0: - resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4245,12 +4207,6 @@ snapshots: '@floating-ui/utils@0.2.8': {} - '@handsontable/pikaday@1.0.0': {} - - '@handsontable/react-wrapper@15.0.0(handsontable@15.0.0)': - dependencies: - handsontable: 15.0.0 - '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4891,9 +4847,6 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@types/trusted-types@2.0.7': - optional: true - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -5101,8 +5054,6 @@ snapshots: bcrypt-ts@5.0.2: {} - bignumber.js@9.1.2: {} - binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -5161,11 +5112,6 @@ snapshots: character-reference-invalid@2.0.1: {} - chevrotain@6.5.0: - dependencies: - regexp-to-ast: 0.4.0 - optional: true - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5228,8 +5174,6 @@ snapshots: cookie@0.7.1: {} - core-js@3.40.0: {} - crelt@1.0.6: {} cross-spawn@7.0.3: @@ -5319,10 +5263,6 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.2.3: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dotenv@16.4.5: {} drizzle-kit@0.25.0: @@ -5908,16 +5848,6 @@ snapshots: graphemer@1.4.0: {} - handsontable@15.0.0: - dependencies: - '@handsontable/pikaday': 1.0.0 - core-js: 3.40.0 - dompurify: 3.2.3 - moment: 2.30.1 - numbro: 2.5.0 - optionalDependencies: - hyperformula: 2.7.1 - has-bigints@1.0.2: {} has-flag@4.0.0: {} @@ -5964,12 +5894,6 @@ snapshots: html-url-attributes@3.0.1: {} - hyperformula@2.7.1: - dependencies: - chevrotain: 6.5.0 - tiny-emitter: 2.1.0 - optional: true - ignore@5.3.2: {} import-fresh@3.3.0: @@ -6605,8 +6529,6 @@ snapshots: minipass@7.1.2: {} - moment@2.30.1: {} - ms@2.1.3: {} mz@2.7.0: @@ -6664,10 +6586,6 @@ snapshots: normalize-path@3.0.0: {} - numbro@2.5.0: - dependencies: - bignumber.js: 9.1.2 - oauth4webapi@3.1.2: {} object-assign@4.1.1: {} @@ -6973,6 +6891,12 @@ snapshots: queue-microtask@1.2.3: {} + react-data-grid@7.0.0-beta.47(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021): + dependencies: + clsx: 2.1.1 + react: 19.0.0-rc-45804af1-20241021 + react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021) + react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021): dependencies: react: 19.0.0-rc-45804af1-20241021 @@ -7050,9 +6974,6 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 - regexp-to-ast@0.4.0: - optional: true - regexp.prototype.flags@1.5.3: dependencies: call-bind: 1.0.7 @@ -7384,9 +7305,6 @@ snapshots: throttleit@2.1.0: {} - tiny-emitter@2.1.0: - optional: true - to-regex-range@5.0.1: dependencies: is-number: 7.0.0