|
| 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; |
0 commit comments