Skip to content

Commit 68498fc

Browse files
committed
feat: show export dropdown button directly in history list
- Always display dropdown export button, even when data is not loaded - Clicking export option will load data and export automatically - Remove simple button, use full dropdown interface from start - Add export button styles to HistoryList.css - Improve data loading logic to fetch directly from API when needed
1 parent 93bc1b3 commit 68498fc

File tree

2 files changed

+177
-70
lines changed

2 files changed

+177
-70
lines changed

src/client/components/HistoryList.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,68 @@
331331
word-break: break-all;
332332
line-height: 1.6;
333333
}
334+
335+
/* Export button styles (reused from ExportButton.css) */
336+
.export-button-container {
337+
position: relative;
338+
display: inline-block;
339+
}
340+
341+
.export-button {
342+
display: flex;
343+
align-items: center;
344+
gap: var(--space-2);
345+
position: relative;
346+
}
347+
348+
.export-arrow {
349+
font-size: 0.625rem;
350+
color: var(--text-tertiary);
351+
margin-left: var(--space-1);
352+
}
353+
354+
.export-overlay {
355+
position: fixed;
356+
top: 0;
357+
left: 0;
358+
width: 100%;
359+
height: 100%;
360+
z-index: 998;
361+
}
362+
363+
.export-dropdown {
364+
position: absolute;
365+
top: calc(100% + var(--space-2));
366+
right: 0;
367+
background: var(--bg-elevated);
368+
border: 1px solid var(--border-default);
369+
border-radius: 4px;
370+
min-width: 120px;
371+
z-index: 999;
372+
box-shadow: var(--shadow-md);
373+
overflow: hidden;
374+
}
375+
376+
.export-option {
377+
display: block;
378+
width: 100%;
379+
padding: var(--space-3) var(--space-4);
380+
background: transparent;
381+
border: none;
382+
color: var(--text-secondary);
383+
font-family: var(--font-mono);
384+
font-size: 0.875rem;
385+
text-align: left;
386+
cursor: pointer;
387+
transition: all var(--transition-fast);
388+
}
389+
390+
.export-option:hover {
391+
background: var(--bg-surface);
392+
color: var(--text-primary);
393+
}
394+
395+
.export-option:focus-visible {
396+
outline: 2px solid var(--color-terminal-green);
397+
outline-offset: -2px;
398+
}

src/client/components/HistoryList.tsx

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { useState, useCallback } from 'react';
1+
import { useState, useCallback, useRef } from 'react';
22
import { useTranslation } from '../hooks/useTranslation';
3-
import { ExportButton } from './ExportButton';
43
import { HealthCheckStatus } from './HealthCheckStatus';
54
import {
65
type ExportData,
@@ -28,96 +27,139 @@ function HistoryExportButtonWrapper({
2827
}: HistoryExportButtonWrapperProps) {
2928
const { t } = useTranslation();
3029
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);
3233

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) {
3743
setIsLoading(true);
3844
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
3953
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);
4373
setIsLoading(false);
44-
}, 100);
74+
return;
75+
}
4576
} catch (error) {
4677
console.error('Failed to load results:', error);
78+
setExporting(false);
4779
setIsLoading(false);
4880
return;
81+
} finally {
82+
setIsLoading(false);
4983
}
5084
}
51-
return;
52-
}
5385

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+
}
6799

68-
const mimeType = getMimeType(format);
69-
const extension = getFileExtension(format);
100+
const mimeType = getMimeType(format);
101+
const extension = getFileExtension(format);
70102

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;
75107

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;
78110

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+
}
84120
};
85121

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}>
90131
<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}
107135
aria-label={t('common.export')}
136+
aria-expanded={isOpen}
108137
>
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>
110144
</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>
121163
);
122164
}
123165

0 commit comments

Comments
 (0)