Skip to content

Commit 93a9449

Browse files
committed
initial implementation
1 parent 44cd2bc commit 93a9449

File tree

4 files changed

+341
-6
lines changed

4 files changed

+341
-6
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
[![Duplicated Lines (%)](https://sonarqube.eea.europa.eu/api/project_badges/measure?project=volto-react-table-widget-develop&metric=duplicated_lines_density)](https://sonarqube.eea.europa.eu/dashboard?id=volto-react-table-widget-develop)
1616

1717

18-
[Volto](https://github.com/plone/volto) add-on
18+
[Volto](https://github.com/plone/volto) add-on to provide a [react-table](https://react-table.tanstack.com/) based widget for Volto to use it with fields with a large set of values.
19+
20+
The widget can be used like Volto's [ObjectListWidget](https://docs.voltocms.com/storybook/?path=/story/widgets-object-list-json--default&globals=measureEnabled:false), but it is more performant when you have a large set of values and provides CSV import and export using the powerwful [react-papaparse](https://www.npmjs.com/package/react-papaparse) library.
1921

2022
## Features
2123

Diff for: package.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"description": "@eeacms/volto-react-table-widget: Volto add-on",
55
"main": "src/index.js",
6-
"author": "European Environment Agency: IDM2 A-Team",
6+
"author": "European Environment Agency: CLMS Team",
77
"license": "MIT",
88
"homepage": "https://github.com/eea/volto-react-table-widget",
99
"keywords": [
@@ -17,7 +17,9 @@
1717
"url": "[email protected]:eea/volto-react-table-widget.git"
1818
},
1919
"dependencies": {
20-
"@plone/scripts": "*"
20+
"@plone/scripts": "*",
21+
"react-table": "*",
22+
"react-papaparse": "*"
2123
},
2224
"devDependencies": {
2325
"@cypress/code-coverage": "^3.9.5",
@@ -41,6 +43,3 @@
4143
"cypress:open": "if [ -d ./project ]; then NODE_ENV=development ./project/node_modules/cypress/bin/cypress open; else NODE_ENV=development ../../../node_modules/cypress/bin/cypress open; fi"
4244
}
4345
}
44-
n/cypress open; else NODE_ENV=development ../../../node_modules/cypress/bin/cypress open; fi"
45-
}
46-
}

Diff for: src/components/Widgets/ReactTableWidget.jsx

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import React from 'react';
2+
import { useTable, usePagination } from 'react-table';
3+
import { useCSVReader, useCSVDownloader } from 'react-papaparse';
4+
import { Toast } from '@plone/volto/components';
5+
import { toast } from 'react-toastify';
6+
import { v4 as uuid } from 'uuid';
7+
import { defineMessages, useIntl } from 'react-intl';
8+
9+
const messages = defineMessages({
10+
template: {
11+
id: 'Variation',
12+
defaultMessage: 'Variation',
13+
},
14+
csv_file_imported_correctly: {
15+
id: 'CSV file imported correctly',
16+
defaultMessage: 'CSV file imported correctly',
17+
},
18+
import_new_imported_item_count: {
19+
id: 'Imported item count',
20+
defaultMessage: '{count} new items imported',
21+
},
22+
import_modified_item_count: {
23+
id: 'Modified item count',
24+
defaultMessage: '{count} items modified',
25+
},
26+
import_csv_file: {
27+
id: 'Import CSV file',
28+
defaultMessage: 'Import CSV file',
29+
},
30+
});
31+
32+
// Create an editable cell renderer
33+
const EditableCell = ({
34+
value: initialValue,
35+
row: { index },
36+
column: { id },
37+
updateMyData, // This is a custom function that we supplied to our table instance
38+
}) => {
39+
// We need to keep and update the state of the cell normally
40+
const [value, setValue] = React.useState(initialValue);
41+
42+
const onChange = (e) => {
43+
setValue(e.target.value);
44+
};
45+
46+
// We'll only update the external data when the input is blurred
47+
const onBlur = () => {
48+
updateMyData(index, id, value);
49+
};
50+
51+
// If the initialValue is changed external, sync it up with our state
52+
React.useEffect(() => {
53+
setValue(initialValue);
54+
}, [initialValue]);
55+
56+
return <input value={value} onChange={onChange} onBlur={onBlur} />;
57+
};
58+
59+
const defaultColumn = {
60+
Cell: EditableCell,
61+
};
62+
63+
// Be sure to pass our updateMyData and the skipPageReset option
64+
function Table({ columns, data, updateMyData, skipPageReset }) {
65+
// For this example, we're using pagination to illustrate how to stop
66+
// the current page from resetting when our data changes
67+
// Otherwise, nothing is different here.
68+
const {
69+
getTableProps,
70+
getTableBodyProps,
71+
headerGroups,
72+
prepareRow,
73+
page,
74+
canPreviousPage,
75+
canNextPage,
76+
pageOptions,
77+
pageCount,
78+
gotoPage,
79+
nextPage,
80+
previousPage,
81+
setPageSize,
82+
state: { pageIndex, pageSize },
83+
} = useTable(
84+
{
85+
columns,
86+
data,
87+
defaultColumn,
88+
// use the skipPageReset option to disable page resetting temporarily
89+
autoResetPage: !skipPageReset,
90+
// updateMyData isn't part of the API, but
91+
// anything we put into these options will
92+
// automatically be available on the instance.
93+
// That way we can call this function from our
94+
// cell renderer!
95+
updateMyData,
96+
},
97+
usePagination,
98+
);
99+
100+
// Render the UI for your table
101+
return (
102+
<>
103+
<table {...getTableProps()}>
104+
<thead>
105+
{headerGroups.map((headerGroup) => (
106+
<tr {...headerGroup.getHeaderGroupProps()}>
107+
{headerGroup.headers.map((column) => (
108+
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
109+
))}
110+
</tr>
111+
))}
112+
</thead>
113+
<tbody {...getTableBodyProps()}>
114+
{page.map((row, i) => {
115+
prepareRow(row);
116+
return (
117+
<tr {...row.getRowProps()}>
118+
{row.cells.map((cell) => {
119+
return (
120+
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
121+
);
122+
})}
123+
</tr>
124+
);
125+
})}
126+
</tbody>
127+
</table>
128+
<div className="pagination">
129+
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
130+
{'<<'}
131+
</button>{' '}
132+
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
133+
{'<'}
134+
</button>{' '}
135+
<button onClick={() => nextPage()} disabled={!canNextPage}>
136+
{'>'}
137+
</button>{' '}
138+
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
139+
{'>>'}
140+
</button>{' '}
141+
<span>
142+
Page{' '}
143+
<strong>
144+
{pageIndex + 1} of {pageOptions.length}
145+
</strong>{' '}
146+
</span>
147+
<span>
148+
| Go to page:{' '}
149+
<input
150+
type="number"
151+
defaultValue={pageIndex + 1}
152+
onChange={(e) => {
153+
const page = e.target.value ? Number(e.target.value) - 1 : 0;
154+
gotoPage(page);
155+
}}
156+
style={{ width: '100px' }}
157+
/>
158+
</span>{' '}
159+
<select
160+
value={pageSize}
161+
onChange={(e) => {
162+
setPageSize(Number(e.target.value));
163+
}}
164+
>
165+
{[10, 20, 30, 40, 50].map((pageSize) => (
166+
<option key={pageSize} value={pageSize}>
167+
Show {pageSize}
168+
</option>
169+
))}
170+
</select>
171+
</div>
172+
</>
173+
);
174+
}
175+
176+
function ReactDataTableWidget(props) {
177+
// Set our editable cell renderer as the default Cell renderer
178+
let { columns, items, csvexport, csvimport } = props;
179+
180+
const intl = useIntl();
181+
const tablecolumns = React.useMemo(() => columns, [columns]);
182+
183+
const [data, setData] = React.useState(() => items);
184+
const [originalData] = React.useState(data);
185+
const [skipPageReset, setSkipPageReset] = React.useState(false);
186+
187+
// We need to keep the table from resetting the pageIndex when we
188+
// Update data. So we can keep track of that flag with a ref.
189+
190+
// When our cell renderer calls updateMyData, we'll use
191+
// the rowIndex, columnId and new value to update the
192+
// original data
193+
const updateMyData = (rowIndex, columnId, value) => {
194+
// We also turn on the flag to not reset the page
195+
setSkipPageReset(true);
196+
setData((old) =>
197+
old.map((row, index) => {
198+
if (index === rowIndex) {
199+
return {
200+
...old[rowIndex],
201+
[columnId]: value,
202+
};
203+
}
204+
return row;
205+
}),
206+
);
207+
208+
// return items back to the component
209+
items = items.map((row, index) => {
210+
if (index === rowIndex) {
211+
return {
212+
...items[rowIndex],
213+
[columnId]: value,
214+
};
215+
}
216+
return row;
217+
});
218+
};
219+
220+
// After data chagnes, we turn the flag back off
221+
// so that if data actually changes when we're not
222+
// editing it, the page is reset
223+
React.useEffect(() => {
224+
setSkipPageReset(false);
225+
}, [data]);
226+
227+
// Let's add a data resetter/randomizer to help
228+
// illustrate that flow...
229+
const resetData = () => setData(originalData);
230+
231+
const csvcolumns = tablecolumns[0].columns.map((d) => {
232+
return {
233+
label: d.accessor,
234+
key: d.accessor,
235+
};
236+
});
237+
238+
csvcolumns.push({
239+
label: '@id',
240+
key: '@id',
241+
});
242+
243+
const { CSVReader } = useCSVReader();
244+
const { CSVDownloader, Type } = useCSVDownloader();
245+
return (
246+
<>
247+
<button onClick={resetData}>Reset Data</button>
248+
{csvexport && (
249+
<CSVDownloader
250+
type={Type.Button}
251+
filename={'prepackaged-files.csv'}
252+
config={{
253+
delimiter: ';',
254+
quoteChar: '"',
255+
}}
256+
data={data}
257+
>
258+
Download as CSV file
259+
</CSVDownloader>
260+
)}
261+
262+
{csvimport && (
263+
<CSVReader
264+
onUploadAccepted={(results) => {
265+
let newdatacount = 0;
266+
267+
let newdata = results.data.map((item) => {
268+
if (!item['@id']) {
269+
newdatacount += 1;
270+
return {
271+
...item,
272+
'@id': uuid(),
273+
};
274+
}
275+
return item;
276+
});
277+
278+
let modifiedcount = newdata.length - newdatacount;
279+
280+
setData(newdata);
281+
props.value.items = newdata;
282+
toast.success(
283+
<Toast
284+
success
285+
autoClose={5000}
286+
content={
287+
(intl.formatMessage(messages.csv_file_imported_correctly) +
288+
' ',
289+
+intl.formatMessage(messages.import_new_imported_item_count, {
290+
count: newdatacount,
291+
}) + ' ',
292+
+intl.formatMessage(messages.import_modified_item_count, {
293+
count: modifiedcount,
294+
}))
295+
}
296+
/>,
297+
);
298+
}}
299+
config={{ header: true }}
300+
>
301+
{({
302+
getRootProps,
303+
acceptedFile,
304+
ProgressBar,
305+
getRemoveFileProps,
306+
}) => (
307+
<>
308+
<div>
309+
<button type="button" {...getRootProps()}>
310+
intl.formatMessage(messages.import_csv_file)
311+
</button>
312+
<div>{acceptedFile && acceptedFile.name}</div>
313+
<button {...getRemoveFileProps()}>Remove</button>
314+
</div>
315+
<ProgressBar />
316+
</>
317+
)}
318+
</CSVReader>
319+
)}
320+
321+
<Table
322+
columns={tablecolumns}
323+
data={data}
324+
updateMyData={updateMyData}
325+
skipPageReset={skipPageReset}
326+
/>
327+
</>
328+
);
329+
}
330+
331+
export default ReactDataTableWidget;

Diff for: src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import ReactTableWidget from './components/Widgets/ReactTableWidget';
2+
export { ReactTableWidget };
3+
14
const applyConfig = (config) => {
25
return config;
36
};

0 commit comments

Comments
 (0)