mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: Add CsvViewer
This commit is contained in:
parent
189ea0efd5
commit
066460a0a5
5 changed files with 179 additions and 3 deletions
21
client/package-lock.json
generated
21
client/package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"@gravity-ui/markdown-editor": "^15.11.0",
|
"@gravity-ui/markdown-editor": "^15.11.0",
|
||||||
"@gravity-ui/uikit": "^7.11.0",
|
"@gravity-ui/uikit": "^7.11.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
|
"@types/papaparse": "^5.3.16",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"photoswipe": "^5.4.4",
|
"photoswipe": "^5.4.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
@ -4745,7 +4747,6 @@
|
||||||
"version": "22.15.17",
|
"version": "22.15.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
|
||||||
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
|
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
|
@ -4756,6 +4757,15 @@
|
||||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.14",
|
"version": "15.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
"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": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
|
@ -14677,8 +14693,7 @@
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
"@gravity-ui/markdown-editor": "^15.11.0",
|
"@gravity-ui/markdown-editor": "^15.11.0",
|
||||||
"@gravity-ui/uikit": "^7.11.0",
|
"@gravity-ui/uikit": "^7.11.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
|
"@types/papaparse": "^5.3.16",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"photoswipe": "^5.4.4",
|
"photoswipe": "^5.4.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
|
135
client/src/components/attachments/Attachments/CsvViewer.jsx
Normal file
135
client/src/components/attachments/Attachments/CsvViewer.jsx
Normal file
|
@ -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 = (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Table celled compact>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
{csvData.rows[0].map((header, index) => (
|
||||||
|
<Table.HeaderCell key={index}>{header}</Table.HeaderCell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{currentRows.slice(1).map((row, rowIndex) => (
|
||||||
|
<Table.Row key={rowIndex}>
|
||||||
|
{row.map((cell, cellIndex) => (
|
||||||
|
<Table.Cell key={cellIndex}>{cell}</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
activePage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
firstItem={null}
|
||||||
|
lastItem={null}
|
||||||
|
pointing
|
||||||
|
secondary
|
||||||
|
boundaryRange={1}
|
||||||
|
siblingRange={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Frame
|
||||||
|
head={<style>{frameStyles.join('')}</style>}
|
||||||
|
className={classNames(styles.wrapper, className)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Frame>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
/* eslint-enable react/no-array-index-key */
|
||||||
|
|
||||||
|
CsvViewer.propTypes = {
|
||||||
|
src: PropTypes.string.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
CsvViewer.defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CsvViewer;
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import Encodings from '../../../constants/Encodings';
|
||||||
import { AttachmentTypes } from '../../../constants/Enums';
|
import { AttachmentTypes } from '../../../constants/Enums';
|
||||||
import ItemContent from './ItemContent';
|
import ItemContent from './ItemContent';
|
||||||
import ContentViewer from './ContentViewer';
|
import ContentViewer from './ContentViewer';
|
||||||
|
import CsvViewer from './CsvViewer';
|
||||||
|
|
||||||
import styles from './Item.module.scss';
|
import styles from './Item.module.scss';
|
||||||
|
|
||||||
|
@ -67,6 +68,15 @@ const Item = React.memo(({ id, isVisible }) => {
|
||||||
<video controls src={attachment.data.url} className={styles.content} />
|
<video controls src={attachment.data.url} className={styles.content} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'text/csv':
|
||||||
|
content = (
|
||||||
|
<CsvViewer
|
||||||
|
src={attachment.data.url}
|
||||||
|
className={classNames(styles.content, styles.contentViewer)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (attachment.data.encoding === Encodings.UTF8) {
|
if (attachment.data.encoding === Encodings.UTF8) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue