diff --git a/package-lock.json b/package-lock.json index 621d10ba..82c5d903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13812,6 +13812,14 @@ "node": ">= 10.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -15480,6 +15488,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -15803,6 +15823,14 @@ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "dev": true }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -16291,6 +16319,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -20129,6 +20168,14 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -32795,6 +32842,18 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-data-grid": { + "version": "7.0.0-beta.34", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.34.tgz", + "integrity": "sha512-OwOP77vabb47xhbvHCXJ2QwFHsfduoLJhgXncGe0gIjrrwJ9xr2SdfEe/U6KAfHnLuBDxCDoY0SOgfJiOQtd+Q==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": "^18.0", + "react-dom": "^18.0" + } + }, "node_modules/react-docgen": { "version": "6.0.0-alpha.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-6.0.0-alpha.3.tgz", @@ -34962,6 +35021,17 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -38957,6 +39027,22 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -39063,6 +39149,26 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -39635,7 +39741,7 @@ }, "packages/components": { "name": "@portaljs/components", - "version": "0.1.12", + "version": "0.2.0", "dependencies": { "@githubocto/flat-ui": "^0.14.1", "@heroicons/react": "^2.0.17", @@ -39648,6 +39754,7 @@ "ol": "^7.4.0", "papaparse": "^5.4.1", "react": "^18.2.0", + "react-data-grid": "^7.0.0-beta.34", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", "react-leaflet": "^4.2.1", @@ -39655,7 +39762,8 @@ "react-vega": "^7.6.0", "vega": "5.25.0", "vega-lite": "5.1.0", - "vitest": "^0.31.4" + "vitest": "^0.31.4", + "xlsx": "^0.18.5" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.7", @@ -46884,6 +46992,7 @@ "postcss-import-url": "^7.2.0", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-data-grid": "*", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", "react-leaflet": "^4.2.1", @@ -46897,7 +47006,8 @@ "vite": "^4.3.2", "vite-plugin-dts": "^2.3.0", "vite-plugin-swc-only": "^0.1.18", - "vitest": "^0.31.4" + "vitest": "^0.31.4", + "xlsx": "^0.18.5" }, "dependencies": { "@eslint/eslintrc": { @@ -50929,6 +51039,11 @@ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", "dev": true }, + "adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -52180,6 +52295,15 @@ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" }, + "cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "requires": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + } + }, "chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -52403,6 +52527,11 @@ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "dev": true }, + "codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -52804,6 +52933,11 @@ "yaml": "^1.10.0" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -55765,6 +55899,11 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true }, + "frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==" + }, "fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -65187,6 +65326,14 @@ "dev": true, "requires": {} }, + "react-data-grid": { + "version": "7.0.0-beta.34", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-7.0.0-beta.34.tgz", + "integrity": "sha512-OwOP77vabb47xhbvHCXJ2QwFHsfduoLJhgXncGe0gIjrrwJ9xr2SdfEe/U6KAfHnLuBDxCDoY0SOgfJiOQtd+Q==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-docgen": { "version": "6.0.0-alpha.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-6.0.0-alpha.3.tgz", @@ -66857,6 +67004,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "requires": { + "frac": "~1.1.2" + } + }, "sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -69873,6 +70028,16 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==" + }, + "word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -69946,6 +70111,20 @@ "devOptional": true, "requires": {} }, + "xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "requires": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + } + }, "xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/packages/components/package.json b/packages/components/package.json index 51fe1b37..a88b8671 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -37,6 +37,7 @@ "ol": "^7.4.0", "papaparse": "^5.4.1", "react": "^18.2.0", + "react-data-grid": "^7.0.0-beta.34", "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", "react-leaflet": "^4.2.1", @@ -44,7 +45,8 @@ "react-vega": "^7.6.0", "vega": "5.25.0", "vega-lite": "5.1.0", - "vitest": "^0.31.4" + "vitest": "^0.31.4", + "xlsx": "^0.18.5" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.7", diff --git a/packages/components/src/components/Excel.tsx b/packages/components/src/components/Excel.tsx new file mode 100644 index 00000000..1a66e399 --- /dev/null +++ b/packages/components/src/components/Excel.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from 'react'; +import LoadingSpinner from './LoadingSpinner'; +import { read, utils } from 'xlsx'; +import DataGrid, { Column, textEditor } from 'react-data-grid'; +import 'react-data-grid/lib/styles.css'; + +export type ExcelProps = { + url: string; +}; + +export function Excel({ url }: ExcelProps) { + const [isLoading, setIsLoading] = useState(false); + const [rows, setRows] = useState(); + const [cols, setCols] = useState(); + + useEffect(() => { + setIsLoading(true); + + fetch(url) + .then((res) => res.arrayBuffer()) + .then((f) => { + const wb = read(f); + const ws = wb.Sheets[wb.SheetNames[0]]; + + const rows = utils.sheet_to_json(ws, { header: 1 }); + + const range = utils.decode_range(ws['!ref'] || 'A1'); + const columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({ + key: String(i), + name: utils.encode_col(i), + editor: textEditor, + })); + + + setRows(rows); + setCols(columns); + + setIsLoading(false); + }); + }, []); + + return isLoading ? ( +
+ +
+ ) : ( + <> + {cols && rows && ( + + )} + + ); +} diff --git a/packages/components/stories/Excel.stories.ts b/packages/components/stories/Excel.stories.ts new file mode 100644 index 00000000..9ef67d1a --- /dev/null +++ b/packages/components/stories/Excel.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Excel, ExcelProps } from '../src/components/Excel'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: 'Components/Excel', + component: Excel, + tags: ['autodocs'], + argTypes: { + url: { + description: + 'Data to be displayed.\n\n E.g.: [["1990", 1], ["1991", 2]] \n\nOR\n\n "https://url.to/data.csv"', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const FromURL: Story = { + name: 'Excel spreadsheet from URL', + args: { + url: 'https://sheetjs.com/pres.xlsx', + }, +};