|
1 | | -import { useState, useCallback } from 'react'; |
| 1 | +import { useState, useCallback, useRef } from 'react'; |
2 | 2 | import { useTranslation } from '../hooks/useTranslation'; |
3 | | -import { ExportButton } from './ExportButton'; |
4 | 3 | import { HealthCheckStatus } from './HealthCheckStatus'; |
5 | 4 | import { |
6 | 5 | type ExportData, |
@@ -28,96 +27,139 @@ function HistoryExportButtonWrapper({ |
28 | 27 | }: HistoryExportButtonWrapperProps) { |
29 | 28 | const { t } = useTranslation(); |
30 | 29 | const [isLoading, setIsLoading] = useState(false); |
31 | | - const [dataLoaded, setDataLoaded] = useState(false); |
| 30 | + const [isOpen, setIsOpen] = useState(false); |
| 31 | + const [exporting, setExporting] = useState(false); |
| 32 | + const buttonRef = useRef<HTMLDivElement>(null); |
32 | 33 |
|
33 | | - const handleExportClick = async (format: ExportFormat) => { |
34 | | - // If data is not loaded yet, load it first |
35 | | - if (!exportData) { |
36 | | - if (!isLoading) { |
| 34 | + const handleExport = async (format: ExportFormat) => { |
| 35 | + setIsOpen(false); |
| 36 | + setExporting(true); |
| 37 | + |
| 38 | + try { |
| 39 | + // Get data to export - use existing exportData or fetch from API |
| 40 | + let dataToExport = exportData; |
| 41 | + |
| 42 | + if (!dataToExport) { |
37 | 43 | setIsLoading(true); |
38 | 44 | try { |
| 45 | + // Fetch data directly from API |
| 46 | + const response = await fetch(`/api/history/${historyId}/results`); |
| 47 | + if (!response.ok) { |
| 48 | + throw new Error('Failed to fetch results'); |
| 49 | + } |
| 50 | + const data = await response.json(); |
| 51 | + |
| 52 | + // Also trigger parent's loadResults to update state |
39 | 53 | await onLoadResults(); |
40 | | - setDataLoaded(true); |
41 | | - // After loading, wait a bit for state to update, then retry export |
42 | | - setTimeout(() => { |
| 54 | + |
| 55 | + // Extract all results from the response |
| 56 | + const allResults: unknown[] = []; |
| 57 | + data.forEach((result: { result_data?: unknown }) => { |
| 58 | + const resultData = result.result_data; |
| 59 | + if (resultData && typeof resultData === 'object' && resultData !== null) { |
| 60 | + if ('results' in resultData && Array.isArray(resultData.results)) { |
| 61 | + allResults.push(...resultData.results); |
| 62 | + } else if (Array.isArray(resultData)) { |
| 63 | + allResults.push(...resultData); |
| 64 | + } |
| 65 | + } |
| 66 | + }); |
| 67 | + |
| 68 | + if (allResults.length > 0) { |
| 69 | + dataToExport = { results: allResults }; |
| 70 | + } else { |
| 71 | + console.error('No results to export'); |
| 72 | + setExporting(false); |
43 | 73 | setIsLoading(false); |
44 | | - }, 100); |
| 74 | + return; |
| 75 | + } |
45 | 76 | } catch (error) { |
46 | 77 | console.error('Failed to load results:', error); |
| 78 | + setExporting(false); |
47 | 79 | setIsLoading(false); |
48 | 80 | return; |
| 81 | + } finally { |
| 82 | + setIsLoading(false); |
49 | 83 | } |
50 | 84 | } |
51 | | - return; |
52 | | - } |
53 | 85 |
|
54 | | - // Data is loaded, proceed with export |
55 | | - let content = ''; |
56 | | - switch (format) { |
57 | | - case 'json': |
58 | | - content = JSON.stringify(exportData, null, 2); |
59 | | - break; |
60 | | - case 'txt': |
61 | | - content = convertToTXT(exportData); |
62 | | - break; |
63 | | - case 'csv': |
64 | | - content = convertToCSV(exportData); |
65 | | - break; |
66 | | - } |
| 86 | + // Data is loaded, proceed with export |
| 87 | + let content = ''; |
| 88 | + switch (format) { |
| 89 | + case 'json': |
| 90 | + content = JSON.stringify(dataToExport, null, 2); |
| 91 | + break; |
| 92 | + case 'txt': |
| 93 | + content = convertToTXT(dataToExport); |
| 94 | + break; |
| 95 | + case 'csv': |
| 96 | + content = convertToCSV(dataToExport); |
| 97 | + break; |
| 98 | + } |
67 | 99 |
|
68 | | - const mimeType = getMimeType(format); |
69 | | - const extension = getFileExtension(format); |
| 100 | + const mimeType = getMimeType(format); |
| 101 | + const extension = getFileExtension(format); |
70 | 102 |
|
71 | | - const blob = new Blob([content], { type: mimeType }); |
72 | | - const url = window.URL.createObjectURL(blob); |
73 | | - const a = document.createElement('a'); |
74 | | - a.href = url; |
| 103 | + const blob = new Blob([content], { type: mimeType }); |
| 104 | + const url = window.URL.createObjectURL(blob); |
| 105 | + const a = document.createElement('a'); |
| 106 | + a.href = url; |
75 | 107 |
|
76 | | - const downloadFilename = ensureFileExtension(`fofa_${historyId}_${Date.now()}`, extension); |
77 | | - a.download = downloadFilename; |
| 108 | + const downloadFilename = ensureFileExtension(`fofa_${historyId}_${Date.now()}`, extension); |
| 109 | + a.download = downloadFilename; |
78 | 110 |
|
79 | | - document.body.appendChild(a); |
80 | | - a.click(); |
81 | | - document.body.removeChild(a); |
82 | | - window.URL.revokeObjectURL(url); |
83 | | - setIsLoading(false); |
| 111 | + document.body.appendChild(a); |
| 112 | + a.click(); |
| 113 | + document.body.removeChild(a); |
| 114 | + window.URL.revokeObjectURL(url); |
| 115 | + } catch (error) { |
| 116 | + console.error('Export error:', error); |
| 117 | + } finally { |
| 118 | + setExporting(false); |
| 119 | + } |
84 | 120 | }; |
85 | 121 |
|
86 | | - // Show button even if data is not loaded yet |
87 | | - // If data is not loaded, show a simple button that loads data on click |
88 | | - if (!exportData) { |
89 | | - return ( |
| 122 | + const formats: { value: ExportFormat; label: string }[] = [ |
| 123 | + { value: 'json', label: 'JSON' }, |
| 124 | + { value: 'txt', label: 'TXT' }, |
| 125 | + { value: 'csv', label: 'CSV' }, |
| 126 | + ]; |
| 127 | + |
| 128 | + // Always show dropdown button, even if data is not loaded |
| 129 | + return ( |
| 130 | + <div className="export-button-container" ref={buttonRef}> |
90 | 131 | <button |
91 | | - className="btn-action" |
92 | | - onClick={async () => { |
93 | | - if (!isLoading) { |
94 | | - setIsLoading(true); |
95 | | - try { |
96 | | - await onLoadResults(); |
97 | | - setDataLoaded(true); |
98 | | - } catch (error) { |
99 | | - console.error('Failed to load results:', error); |
100 | | - } finally { |
101 | | - setIsLoading(false); |
102 | | - } |
103 | | - } |
104 | | - }} |
105 | | - disabled={isLoading} |
106 | | - title={t('common.export')} |
| 132 | + className="btn-secondary export-button" |
| 133 | + onClick={() => setIsOpen(!isOpen)} |
| 134 | + disabled={exporting || isLoading} |
107 | 135 | aria-label={t('common.export')} |
| 136 | + aria-expanded={isOpen} |
108 | 137 | > |
109 | | - {isLoading ? t('common.loading') : t('common.export')} |
| 138 | + {isLoading |
| 139 | + ? t('common.loading') || 'LOADING...' |
| 140 | + : exporting |
| 141 | + ? t('common.exporting') || 'EXPORTING...' |
| 142 | + : t('common.export')} |
| 143 | + <span className="export-arrow">{isOpen ? '▲' : '▼'}</span> |
110 | 144 | </button> |
111 | | - ); |
112 | | - } |
113 | | - |
114 | | - return ( |
115 | | - <ExportButton |
116 | | - data={exportData} |
117 | | - filename={`fofa_${historyId}_${Date.now()}`} |
118 | | - onExportClick={handleExportClick} |
119 | | - isLoading={isLoading} |
120 | | - /> |
| 145 | + {isOpen && ( |
| 146 | + <> |
| 147 | + <div className="export-overlay" onClick={() => setIsOpen(false)} /> |
| 148 | + <div className="export-dropdown"> |
| 149 | + {formats.map(format => ( |
| 150 | + <button |
| 151 | + key={format.value} |
| 152 | + className="export-option" |
| 153 | + onClick={() => handleExport(format.value)} |
| 154 | + aria-label={`${t('common.export')} as ${format.label}`} |
| 155 | + > |
| 156 | + {format.label} |
| 157 | + </button> |
| 158 | + ))} |
| 159 | + </div> |
| 160 | + </> |
| 161 | + )} |
| 162 | + </div> |
121 | 163 | ); |
122 | 164 | } |
123 | 165 |
|
|
0 commit comments