Skip to content

Commit da390ca

Browse files
feat(ui): add universal data export (CSV, JSON, Markdown, SQL dump) to ResultsTab
1 parent 5629736 commit da390ca

3 files changed

Lines changed: 412 additions & 39 deletions

File tree

‎lib/features/main_screen/results_tab.dart‎

Lines changed: 120 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import 'dart:async' show unawaited;
22

33
import 'package:flutter/material.dart' as material;
4-
import 'package:flutter/services.dart' show Clipboard, ClipboardData;
5-
import 'package:querya_desktop/core/csv/result_grid_csv.dart';
6-
import 'package:querya_desktop/core/csv/save_result_grid_csv.dart';
7-
import 'package:querya_desktop/core/json/result_grid_json.dart';
8-
import 'package:querya_desktop/core/json/save_result_grid_json.dart';
94
import 'package:querya_desktop/features/main_screen/result_grid_view.dart';
5+
import 'package:querya_desktop/shared/services/data_export_service.dart';
106
import 'package:querya_desktop/shared/widgets/widgets.dart';
117

128
/// Query output: grid, loading, error, or placeholder.
@@ -84,67 +80,67 @@ class ResultsTab extends StatelessWidget {
8480
size: ButtonSize.small,
8581
onPressed: () {
8682
unawaited(() async {
87-
final outcome = await saveResultGridCsvFile(
83+
await DataExportService.copyToClipboard(
84+
DataExportFormat.csv,
8885
columns: columns,
8986
rows: rows,
9087
);
91-
if (!context.mounted) return;
92-
if (outcome == SaveResultGridCsvOutcome.error) {
93-
await _showSaveFileErrorDialog(context);
94-
}
9588
}());
9689
},
9790
leading: const material.Icon(
98-
material.Icons.save_alt_rounded,
91+
material.Icons.copy_rounded,
9992
size: 14,
10093
),
101-
child: const Text('Save as CSV…'),
94+
child: const Text('Copy as CSV'),
10295
),
10396
OutlineButton(
10497
size: ButtonSize.small,
10598
onPressed: () {
10699
unawaited(() async {
107-
final outcome = await saveResultGridJsonFile(
100+
await DataExportService.copyToClipboard(
101+
DataExportFormat.json,
108102
columns: columns,
109103
rows: rows,
110104
);
111-
if (!context.mounted) return;
112-
if (outcome == SaveResultGridJsonOutcome.error) {
113-
await _showSaveFileErrorDialog(context);
114-
}
115105
}());
116106
},
117107
leading: const material.Icon(
118-
material.Icons.data_object_rounded,
108+
material.Icons.copy_rounded,
119109
size: 14,
120110
),
121-
child: const Text('Save as JSON…'),
111+
child: const Text('Copy as JSON'),
122112
),
123-
OutlineButton(
124-
size: ButtonSize.small,
125-
onPressed: () {
113+
_ExportMenuButton(
114+
label: 'Copy formatted ▾',
115+
icon: material.Icons.copy_all_rounded,
116+
isSave: false,
117+
onSelected: (format) {
126118
unawaited(() async {
127-
final csv = await resultGridAsCsvAsync(columns, rows);
128-
await Clipboard.setData(ClipboardData(text: csv));
119+
await DataExportService.copyToClipboard(
120+
format,
121+
columns: columns,
122+
rows: rows,
123+
);
129124
}());
130125
},
131-
leading: const material.Icon(
132-
material.Icons.copy_rounded,
133-
size: 14,
134-
),
135-
child: const Text('Copy as CSV'),
136126
),
137-
OutlineButton(
138-
size: ButtonSize.small,
139-
onPressed: () {
140-
final json = resultGridAsJson(columns, rows);
141-
Clipboard.setData(ClipboardData(text: json));
127+
_ExportMenuButton(
128+
label: 'Save to file ▾',
129+
icon: material.Icons.save_alt_rounded,
130+
isSave: true,
131+
onSelected: (format) {
132+
unawaited(() async {
133+
final outcome = await DataExportService.saveToFile(
134+
format,
135+
columns: columns,
136+
rows: rows,
137+
);
138+
if (!context.mounted) return;
139+
if (outcome == SaveExportOutcome.error) {
140+
await _showSaveFileErrorDialog(context);
141+
}
142+
}());
142143
},
143-
leading: const material.Icon(
144-
material.Icons.copy_rounded,
145-
size: 14,
146-
),
147-
child: const Text('Copy as JSON'),
148144
),
149145
],
150146
),
@@ -175,3 +171,88 @@ Future<void> _showSaveFileErrorDialog(material.BuildContext context) {
175171
),
176172
);
177173
}
174+
175+
class _ExportMenuButton extends StatelessWidget {
176+
const _ExportMenuButton({
177+
required this.label,
178+
required this.icon,
179+
required this.onSelected,
180+
required this.isSave,
181+
});
182+
183+
final String label;
184+
final material.IconData icon;
185+
final material.ValueChanged<DataExportFormat> onSelected;
186+
final bool isSave;
187+
188+
@override
189+
Widget build(BuildContext context) {
190+
return material.PopupMenuButton<DataExportFormat>(
191+
tooltip: label,
192+
onSelected: onSelected,
193+
itemBuilder: (context) => [
194+
material.PopupMenuItem(
195+
value: DataExportFormat.csv,
196+
child: material.Row(
197+
children: [
198+
const material.Icon(
199+
material.Icons.table_chart_outlined, size: 16),
200+
const material.SizedBox(width: 8),
201+
material.Text(isSave ? 'CSV (.csv)' : 'Copy as CSV'),
202+
],
203+
),
204+
),
205+
material.PopupMenuItem(
206+
value: DataExportFormat.json,
207+
child: material.Row(
208+
children: [
209+
const material.Icon(
210+
material.Icons.data_object_rounded, size: 16),
211+
const material.SizedBox(width: 8),
212+
material.Text(isSave ? 'JSON (.json)' : 'Copy as JSON'),
213+
],
214+
),
215+
),
216+
material.PopupMenuItem(
217+
value: DataExportFormat.markdown,
218+
child: material.Row(
219+
children: [
220+
const material.Icon(material.Icons.code_rounded, size: 16),
221+
const material.SizedBox(width: 8),
222+
material.Text(isSave ? 'Markdown Table (.md)' : 'Copy as Markdown Table'),
223+
],
224+
),
225+
),
226+
material.PopupMenuItem(
227+
value: DataExportFormat.sqlDump,
228+
child: material.Row(
229+
children: [
230+
const material.Icon(material.Icons.storage_rounded, size: 16),
231+
const material.SizedBox(width: 8),
232+
material.Text(isSave ? 'SQL INSERT Dump (.sql)' : 'Copy as SQL Dump'),
233+
],
234+
),
235+
),
236+
],
237+
child: material.Container(
238+
padding: const material.EdgeInsets.symmetric(
239+
horizontal: 10, vertical: 6),
240+
decoration: material.BoxDecoration(
241+
border: material.Border.all(
242+
color: Theme.of(context).colorScheme.border,
243+
),
244+
borderRadius: material.BorderRadius.circular(6),
245+
),
246+
child: material.Row(
247+
mainAxisSize: material.MainAxisSize.min,
248+
children: [
249+
material.Icon(icon,
250+
size: 14, color: Theme.of(context).colorScheme.foreground),
251+
const material.SizedBox(width: 6),
252+
Text(label).small(),
253+
],
254+
),
255+
),
256+
);
257+
}
258+
}

0 commit comments

Comments
 (0)