@@ -10,6 +10,7 @@ import {
1010import {
1111 DownloadIcon ,
1212 FileIcon ,
13+ FileZipIcon ,
1314 TrashIcon ,
1415 UploadIcon ,
1516} from '@primer/octicons-react' ;
@@ -29,6 +30,7 @@ import { getReportSchema } from '../lib/report-schema';
2930import { formatDateRange } from '../lib/formatters' ;
3031import { FileDropzone } from './FileDropzone' ;
3132import { parseCSV } from '../lib/csv-parser' ;
33+ import { createZipArchive , extractCsvsFromZip , isZipFile , ACCEPTED_FILE_TYPES } from '../lib/zip' ;
3234import styles from './CsvManager.module.css' ;
3335import tableStyles from './ReportTable.module.css' ;
3436
@@ -46,6 +48,24 @@ interface FileRow {
4648
4749const columnHelper = createColumnHelper < FileRow > ( ) ;
4850
51+ function ZipInfo ( ) {
52+ return (
53+ < div className = { styles . zipInfo } >
54+ < div className = { styles . zipInfoIcon } >
55+ < FileZipIcon size = { 24 } />
56+ </ div >
57+ < div >
58+ < Text as = "p" className = { styles . zipInfoTitle } > ZIP backup & restore</ Text >
59+ < Text as = "p" className = { styles . zipInfoDesc } >
60+ < strong > Download all</ strong > saves your reports as a single < code > github-reports-YYYY-MM-DD.zip</ code > archive.
61+ To restore, just drop the ZIP file here or click < strong > Add file</ strong > and select it.
62+ All CSVs inside the archive will be imported automatically.
63+ </ Text >
64+ </ div >
65+ </ div >
66+ ) ;
67+ }
68+
4969function SortIcon ( { direction } : { direction : 'asc' | 'desc' | false } ) {
5070 if ( direction === 'asc' ) {
5171 return (
@@ -85,9 +105,15 @@ export function CsvManager() {
85105 const handleAddFile = useCallback (
86106 async ( files : FileList ) => {
87107 for ( const file of Array . from ( files ) ) {
88- const text = await file . text ( ) ;
89- const parsed = parseCSV ( text , file . name ) ;
90- addReport ( parsed , text ) ;
108+ if ( isZipFile ( file ) ) {
109+ const csvFiles = await extractCsvsFromZip ( file ) ;
110+ for ( const { name, content } of csvFiles ) {
111+ addReport ( parseCSV ( content , name ) , content ) ;
112+ }
113+ } else {
114+ const text = await file . text ( ) ;
115+ addReport ( parseCSV ( text , file . name ) , text ) ;
116+ }
91117 }
92118 } ,
93119 [ addReport ] ,
@@ -110,10 +136,20 @@ export function CsvManager() {
110136 ) ;
111137
112138 const handleDownloadAll = useCallback ( ( ) => {
113- reports . forEach ( ( _ , i ) => {
114- setTimeout ( ( ) => handleDownloadFile ( i ) , i * 200 ) ;
139+ const files : Record < string , string > = { } ;
140+ reports . forEach ( ( report , i ) => {
141+ const csv = rawCsvs [ i ] ;
142+ if ( csv ) files [ report . fileName ] = csv ;
115143 } ) ;
116- } , [ reports , handleDownloadFile ] ) ;
144+ const blob = createZipArchive ( files ) ;
145+ const url = URL . createObjectURL ( blob ) ;
146+ const a = document . createElement ( 'a' ) ;
147+ a . href = url ;
148+ const date = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
149+ a . download = `github-reports-${ date } .zip` ;
150+ a . click ( ) ;
151+ URL . revokeObjectURL ( url ) ;
152+ } , [ reports , rawCsvs ] ) ;
117153
118154 const handleDeleteFile = useCallback (
119155 ( index : number ) => {
@@ -255,6 +291,7 @@ export function CsvManager() {
255291 < div className = { styles . emptyState } >
256292 < FileDropzone forceShow reportType = "usage_report" />
257293 </ div >
294+ < ZipInfo />
258295 </ div >
259296 ) ;
260297 }
@@ -297,7 +334,7 @@ export function CsvManager() {
297334 < input
298335 ref = { fileInputRef }
299336 type = "file"
300- accept = ".csv"
337+ accept = { ACCEPTED_FILE_TYPES }
301338 multiple
302339 style = { { display : 'none' } }
303340 onChange = { ( e ) => {
@@ -367,6 +404,8 @@ export function CsvManager() {
367404 </ table >
368405 </ div >
369406
407+ < ZipInfo />
408+
370409 { confirmDeleteAll && (
371410 < Dialog
372411 title = "Delete all files?"
0 commit comments