diff --git a/client/package-lock.json b/client/package-lock.json index 2c27be92..6f912566 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,6 +13,7 @@ "@gravity-ui/markdown-editor": "^15.11.0", "@gravity-ui/uikit": "^7.11.0", "@juggle/resize-observer": "^3.4.0", + "@types/papaparse": "^5.3.16", "@vitejs/plugin-react": "^4.4.1", "browserslist-to-esbuild": "^2.1.1", "classnames": "^2.5.1", @@ -54,6 +55,7 @@ "lowlight": "^3.3.0", "markdown-it": "^13.0.2", "nanoid": "^5.1.5", + "papaparse": "^5.5.3", "patch-package": "^8.0.0", "photoswipe": "^5.4.4", "prop-types": "^15.8.1", @@ -4745,7 +4747,6 @@ "version": "22.15.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", - "devOptional": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4756,6 +4757,15 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/papaparse": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz", + "integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -11227,6 +11237,12 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -14677,8 +14693,7 @@ "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", diff --git a/client/package.json b/client/package.json index ac1528d2..cf8921fd 100755 --- a/client/package.json +++ b/client/package.json @@ -84,6 +84,7 @@ "@gravity-ui/markdown-editor": "^15.11.0", "@gravity-ui/uikit": "^7.11.0", "@juggle/resize-observer": "^3.4.0", + "@types/papaparse": "^5.3.16", "@vitejs/plugin-react": "^4.4.1", "browserslist-to-esbuild": "^2.1.1", "classnames": "^2.5.1", @@ -125,6 +126,7 @@ "lowlight": "^3.3.0", "markdown-it": "^13.0.2", "nanoid": "^5.1.5", + "papaparse": "^5.5.3", "patch-package": "^8.0.0", "photoswipe": "^5.4.4", "prop-types": "^15.8.1", diff --git a/client/src/components/attachments/Attachments/CsvViewer.jsx b/client/src/components/attachments/Attachments/CsvViewer.jsx new file mode 100644 index 00000000..dab29ae2 --- /dev/null +++ b/client/src/components/attachments/Attachments/CsvViewer.jsx @@ -0,0 +1,135 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Pagination, Table } from 'semantic-ui-react'; +import Papa from 'papaparse'; +import Frame from 'react-frame-component'; + +import styles from './CsvViewer.module.scss'; + +const ROWS_PER_PAGE = 50; + +/* eslint-disable react/no-array-index-key */ +const CsvViewer = React.memo(({ src, className }) => { + const [csvData, setCsvData] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + const frameStyles = useMemo( + () => [ + ...Array.from(document.styleSheets).flatMap((styleSheet) => + Array.from(styleSheet.cssRules).map((cssRule) => cssRule.cssText), + ), + 'body{background:rgb(248,248,248);min-width:fit-content;overflow-x:visible}', + '.frame-content{padding:40px}', + '.frame-content>pre{margin:0}', + '.hljs{padding:0}', + '::-webkit-scrollbar{height:10px}', + '.ui.pagination.menu{display:flex;justify-content:center;margin-top:20px;padding:10px 0}', + ], + [], + ); + + const handlePageChange = useCallback((e, { activePage }) => { + setCurrentPage(activePage); + }, []); + + useEffect(() => { + async function fetchFile() { + try { + const response = await fetch(src, { + credentials: 'include', + }); + + const text = await response.text(); + + Papa.parse(text, { + skipEmptyLines: true, + complete: (results) => { + const rows = results.data; + setCsvData({ + rows, + totalRows: rows.length, + }); + }, + }); + } catch (err) { + setCsvData(null); + } + } + + fetchFile(); + }, [src]); + + if (!csvData) { + return null; + } + + const startIdx = (currentPage - 1) * ROWS_PER_PAGE; + const endIdx = startIdx + ROWS_PER_PAGE; + const currentRows = csvData.rows.slice(startIdx, endIdx); + const totalPages = Math.ceil(csvData.totalRows / ROWS_PER_PAGE); + + const content = ( +
+
+ + + + {csvData.rows[0].map((header, index) => ( + {header} + ))} + + + + {currentRows.slice(1).map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + {cell} + ))} + + ))} + +
+
+ {totalPages > 1 && ( + + )} +
+ ); + + return ( + {frameStyles.join('')}} + className={classNames(styles.wrapper, className)} + > + {content} + + ); +}); +/* eslint-enable react/no-array-index-key */ + +CsvViewer.propTypes = { + src: PropTypes.string.isRequired, + className: PropTypes.string, +}; + +CsvViewer.defaultProps = { + className: undefined, +}; + +export default CsvViewer; diff --git a/client/src/components/attachments/Attachments/CsvViewer.module.scss b/client/src/components/attachments/Attachments/CsvViewer.module.scss new file mode 100644 index 00000000..3827f58b --- /dev/null +++ b/client/src/components/attachments/Attachments/CsvViewer.module.scss @@ -0,0 +1,14 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +:global(#app) { + .wrapper { + background: #fff; + border: 0; + display: flex; + flex-direction: column; + height: 100%; + } +} diff --git a/client/src/components/attachments/Attachments/Item.jsx b/client/src/components/attachments/Attachments/Item.jsx index 7fedb769..1efb4035 100644 --- a/client/src/components/attachments/Attachments/Item.jsx +++ b/client/src/components/attachments/Attachments/Item.jsx @@ -16,6 +16,7 @@ import Encodings from '../../../constants/Encodings'; import { AttachmentTypes } from '../../../constants/Enums'; import ItemContent from './ItemContent'; import ContentViewer from './ContentViewer'; +import CsvViewer from './CsvViewer'; import styles from './Item.module.scss'; @@ -68,6 +69,15 @@ const Item = React.memo(({ id, isVisible }) => {