sqlite-hub 0.11.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +52 -13
  2. package/database.sqlite +0 -0
  3. package/frontend/js/api.js +64 -12
  4. package/frontend/js/app.js +723 -37
  5. package/frontend/js/components/emptyState.js +42 -46
  6. package/frontend/js/components/modal.js +526 -2
  7. package/frontend/js/components/queryEditor.js +12 -3
  8. package/frontend/js/components/queryResults.js +79 -17
  9. package/frontend/js/components/rowEditorPanel.js +345 -12
  10. package/frontend/js/components/sidebar.js +106 -23
  11. package/frontend/js/components/tableDesignerEditor.js +69 -11
  12. package/frontend/js/store.js +437 -26
  13. package/frontend/js/utils/copyColumnExport.js +117 -0
  14. package/frontend/js/utils/exportFilenames.js +32 -0
  15. package/frontend/js/utils/filePathPreview.js +315 -0
  16. package/frontend/js/utils/format.js +1 -1
  17. package/frontend/js/utils/rowEditorJson.js +65 -0
  18. package/frontend/js/utils/sqlFormatter.js +691 -0
  19. package/frontend/js/utils/tableDesigner.js +178 -6
  20. package/frontend/js/utils/textCellStats.js +20 -0
  21. package/frontend/js/utils/timestampPreview.js +264 -0
  22. package/frontend/js/views/charts.js +3 -1
  23. package/frontend/js/views/data.js +39 -4
  24. package/frontend/js/views/editor.js +50 -1
  25. package/frontend/js/views/settings.js +1 -4
  26. package/frontend/js/views/structure.js +154 -212
  27. package/frontend/styles/base.css +6 -0
  28. package/frontend/styles/components.css +463 -2
  29. package/frontend/styles/structure-graph.css +0 -3
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/tokens.css +95 -95
  32. package/package.json +2 -3
  33. package/server/routes/export.js +97 -12
  34. package/server/services/sqlite/dataBrowserService.js +2 -68
  35. package/server/services/sqlite/exportService.js +74 -15
  36. package/server/services/sqlite/introspection.js +209 -1
  37. package/server/services/sqlite/sqlExecutor.js +31 -0
  38. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  39. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  40. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  41. package/server/services/sqlite/tableDesignerService.js +1 -1
  42. package/server/services/sqlite/tableFilter.js +75 -0
  43. package/server/utils/csv.js +30 -4
  44. package/tests/check-constraint-options.test.js +90 -0
  45. package/tests/export-filenames.test.js +34 -0
  46. package/tests/file-path-preview.test.js +165 -0
  47. package/tests/row-editor-json.test.js +82 -0
  48. package/tests/row-editor-timestamp-preview.test.js +192 -0
  49. package/tests/sql-formatter.test.js +173 -0
  50. package/tests/sql-highlight.test.js +38 -0
  51. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  52. package/tests/text-cell-stats.test.js +38 -0
@@ -7,6 +7,7 @@ import {
7
7
  hydrateTableDesignerDraft,
8
8
  removeTableDesignerColumn,
9
9
  updateTableDesignerColumnField,
10
+ updateTableDesignerConstraintField,
10
11
  updateTableDesignerDraftField,
11
12
  } from './utils/tableDesigner.js';
12
13
  import {
@@ -22,6 +23,7 @@ import {
22
23
  MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
23
24
  MEDIA_TAGGING_DEFAULT_TAG_TABLE,
24
25
  } from './lib/mediaTaggingDefaults.js';
26
+ import { buildTextExportFilename } from './utils/exportFilenames.js';
25
27
 
26
28
  const listeners = new Set();
27
29
  const DEFAULT_SETTINGS = {
@@ -35,6 +37,9 @@ const DATA_FILTER_OPERATORS = new Set(['=', '!=', '<', '>', '<=', '>=', 'equals'
35
37
  const DATA_ROW_SIZE_STORAGE_KEY = 'data_row_size';
36
38
  const CHARTS_HISTORY_TAB_STORAGE_KEY = 'charts_history_tab';
37
39
  const QUERY_HISTORY_TAB_STORAGE_KEY = 'query_history_tab';
40
+ const COPY_COLUMN_SEPARATOR_STORAGE_KEY = 'sqlitehub.copyColumn.separator';
41
+ const COPY_COLUMN_WRAPPER_STORAGE_KEY = 'sqlitehub.copyColumn.wrapper';
42
+ const COPY_COLUMN_LINE_BREAKS_STORAGE_KEY = 'sqlitehub.copyColumn.lineBreaks';
38
43
  const UI_PREFERENCE_STORAGE_KEYS = {
39
44
  sqlEditorHistoryVisible: 'sqlite_hub_sql_editor_history_visible',
40
45
  sqlEditorEditorVisible: 'sqlite_hub_sql_editor_editor_visible',
@@ -50,6 +55,12 @@ const QUERY_HISTORY_PAGE_SIZE = 30;
50
55
  const QUERY_HISTORY_RUN_LIMIT = 8;
51
56
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
52
57
  const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
58
+ const COPY_COLUMN_MODES = new Set(['column', 'column-with-header', 'first-10', 'markdown-todo']);
59
+ const TEXT_EXPORT_FORMAT_LABELS = {
60
+ csv: 'CSV',
61
+ tsv: 'TSV',
62
+ md: 'Markdown',
63
+ };
53
64
  const MISSING_DATABASE_ERROR = {
54
65
  code: 'ACTIVE_DATABASE_REQUIRED',
55
66
  message: 'No active SQLite database selected.',
@@ -79,6 +90,34 @@ function storeDataPageSize(pageSize) {
79
90
  }
80
91
  }
81
92
 
93
+ function findQueryHistoryItemBySql(sql, snapshot = state) {
94
+ const normalizedSql = String(sql ?? '').trim();
95
+
96
+ if (!normalizedSql) {
97
+ return null;
98
+ }
99
+
100
+ if (String(snapshot.editor.historyDetail?.rawSql ?? '').trim() === normalizedSql) {
101
+ return snapshot.editor.historyDetail;
102
+ }
103
+
104
+ return snapshot.editor.history.find(entry => String(entry.rawSql ?? '').trim() === normalizedSql) ?? null;
105
+ }
106
+
107
+ function getCurrentQueryExportFilename(format = 'csv', filename = '') {
108
+ const queryText = String(state.editor.sqlText ?? '');
109
+ const historyItem = findQueryHistoryItemBySql(queryText);
110
+ const fallback = historyItem?.displayTitle || 'query-results';
111
+
112
+ return buildTextExportFilename(filename || fallback, { format, fallback });
113
+ }
114
+
115
+ function getCurrentDataTableExportFilename(format = 'csv', filename = '') {
116
+ const fallback = state.dataBrowser.selectedTable || 'table';
117
+
118
+ return buildTextExportFilename(filename || fallback, { format, fallback });
119
+ }
120
+
82
121
  function readStoredBoolean(key, fallback) {
83
122
  try {
84
123
  const value = globalThis.localStorage?.getItem(key);
@@ -122,6 +161,27 @@ function storeString(key, value) {
122
161
  }
123
162
  }
124
163
 
164
+ function readCopyColumnPreferences() {
165
+ return {
166
+ separator: readStoredString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, ','),
167
+ wrapper: readStoredString(COPY_COLUMN_WRAPPER_STORAGE_KEY, '"'),
168
+ lineBreaks: readStoredBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, false),
169
+ };
170
+ }
171
+
172
+ function normalizeCopyColumnMode(mode) {
173
+ const normalizedMode = String(mode ?? '').trim();
174
+ return COPY_COLUMN_MODES.has(normalizedMode) ? normalizedMode : 'column';
175
+ }
176
+
177
+ function normalizeCopyColumnScope(scope) {
178
+ return scope === 'charts' ? 'charts' : 'editor';
179
+ }
180
+
181
+ function getResultByCopyColumnScope(scope, snapshot = state) {
182
+ return normalizeCopyColumnScope(scope) === 'charts' ? snapshot.charts.result : snapshot.editor.result;
183
+ }
184
+
125
185
  function readStoredEditorActiveTab(fallback = 'messages') {
126
186
  try {
127
187
  const value = globalThis.localStorage?.getItem(UI_PREFERENCE_STORAGE_KEYS.sqlEditorActiveTab);
@@ -1475,6 +1535,16 @@ async function resolvePendingDataBrowserRow(version) {
1475
1535
  return;
1476
1536
  }
1477
1537
 
1538
+ if (!pendingTarget.identity && Number.isInteger(pendingTarget.rowIndex)) {
1539
+ if (table.rows?.[pendingTarget.rowIndex]) {
1540
+ state.dataBrowser.selectedRowIndex = pendingTarget.rowIndex;
1541
+ state.dataBrowser.selectedRow = null;
1542
+ }
1543
+
1544
+ state.dataBrowser.pendingOpenRow = null;
1545
+ return;
1546
+ }
1547
+
1478
1548
  const matchingRowIndex = findDataBrowserRowIndexByIdentity(table.rows ?? [], pendingTarget.identity);
1479
1549
 
1480
1550
  if (matchingRowIndex >= 0) {
@@ -1978,14 +2048,20 @@ async function previewMediaTaggingDraft(options = {}) {
1978
2048
  }
1979
2049
  }
1980
2050
 
1981
- function invalidateDatabaseCaches() {
2051
+ function invalidateDatabaseCaches(options = {}) {
2052
+ const preserveDataBrowserState = options.preserveDataBrowserState === true;
2053
+
1982
2054
  state.overview.data = null;
1983
2055
  state.dataBrowser.tables = [];
1984
- state.dataBrowser.selectedTable = null;
2056
+ if (!preserveDataBrowserState) {
2057
+ state.dataBrowser.selectedTable = null;
2058
+ }
1985
2059
  state.dataBrowser.table = null;
1986
- state.dataBrowser.page = 1;
1987
- resetDataBrowserTableSearch();
1988
- resetDataBrowserSearch();
2060
+ if (!preserveDataBrowserState) {
2061
+ state.dataBrowser.page = 1;
2062
+ resetDataBrowserTableSearch();
2063
+ resetDataBrowserSearch();
2064
+ }
1989
2065
  clearDataBrowserRowSelectionState();
1990
2066
  state.dataBrowser.pendingOpenRow = null;
1991
2067
  state.dataBrowser.exportLoading = false;
@@ -2244,15 +2320,131 @@ export async function setRoute(route) {
2244
2320
  await loadRouteData(route);
2245
2321
  }
2246
2322
 
2247
- export function openModal(kind) {
2323
+ export function openModal(kind, options = {}) {
2248
2324
  state.modal = {
2249
2325
  kind,
2250
2326
  error: null,
2251
2327
  submitting: false,
2328
+ ...options,
2329
+ };
2330
+ emitChange();
2331
+ }
2332
+
2333
+ export function openQueryExportModal() {
2334
+ if (!String(state.editor.sqlText ?? '').trim()) {
2335
+ pushToast('Enter a query before exporting.', 'alert');
2336
+ return;
2337
+ }
2338
+
2339
+ state.modal = {
2340
+ kind: 'query-export',
2341
+ filename: getCurrentQueryExportFilename('csv'),
2342
+ error: null,
2343
+ submitting: false,
2344
+ };
2345
+ emitChange();
2346
+ }
2347
+
2348
+ export function openDataExportModal() {
2349
+ if (!state.dataBrowser.selectedTable) {
2350
+ pushToast('No table selected for export.', 'alert');
2351
+ return;
2352
+ }
2353
+
2354
+ state.modal = {
2355
+ kind: 'data-export',
2356
+ filename: getCurrentDataTableExportFilename('csv'),
2357
+ error: null,
2358
+ submitting: false,
2252
2359
  };
2253
2360
  emitChange();
2254
2361
  }
2255
2362
 
2363
+ export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
2364
+ const resultScope = normalizeCopyColumnScope(scope);
2365
+ const normalizedColumnName = String(columnName ?? '');
2366
+ const result = getResultByCopyColumnScope(resultScope);
2367
+ const hasColumn = (result?.columns ?? []).some(column => String(column) === normalizedColumnName);
2368
+
2369
+ if (!hasColumn) {
2370
+ pushToast('Column could not be found in the current result set.', 'alert');
2371
+ return;
2372
+ }
2373
+
2374
+ const preferences = readCopyColumnPreferences();
2375
+
2376
+ state.modal = {
2377
+ kind: 'copy-column',
2378
+ scope: resultScope,
2379
+ columnName: normalizedColumnName,
2380
+ copyMode: normalizeCopyColumnMode(mode),
2381
+ separator: preferences.separator,
2382
+ wrapper: preferences.wrapper,
2383
+ lineBreaks: preferences.lineBreaks,
2384
+ error: null,
2385
+ submitting: false,
2386
+ };
2387
+ emitChange();
2388
+ }
2389
+
2390
+ export function storeCopyColumnPreferences({ separator = ',', wrapper = '"', lineBreaks = false } = {}) {
2391
+ storeString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, separator);
2392
+ storeString(COPY_COLUMN_WRAPPER_STORAGE_KEY, wrapper);
2393
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, lineBreaks);
2394
+
2395
+ if (state.modal?.kind === 'copy-column') {
2396
+ state.modal.separator = String(separator ?? '');
2397
+ state.modal.wrapper = String(wrapper ?? '');
2398
+ state.modal.lineBreaks = Boolean(lineBreaks);
2399
+ }
2400
+ }
2401
+
2402
+ export function updateCopyColumnModalFormatField(field, value) {
2403
+ if (state.modal?.kind !== 'copy-column') {
2404
+ return;
2405
+ }
2406
+
2407
+ const normalizedField = String(field ?? '').trim();
2408
+
2409
+ if (normalizedField !== 'separator' && normalizedField !== 'wrapper' && normalizedField !== 'lineBreaks') {
2410
+ return;
2411
+ }
2412
+
2413
+ const normalizedValue = normalizedField === 'lineBreaks' ? Boolean(value) : String(value ?? '');
2414
+ state.modal[normalizedField] = normalizedValue;
2415
+ state.modal.error = null;
2416
+
2417
+ if (normalizedField === 'lineBreaks') {
2418
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, normalizedValue);
2419
+ } else {
2420
+ storeString(
2421
+ normalizedField === 'separator' ? COPY_COLUMN_SEPARATOR_STORAGE_KEY : COPY_COLUMN_WRAPPER_STORAGE_KEY,
2422
+ normalizedValue,
2423
+ );
2424
+ }
2425
+ emitChange();
2426
+ }
2427
+
2428
+ export function setCopyColumnModalSubmitting(submitting) {
2429
+ if (state.modal?.kind !== 'copy-column') {
2430
+ return;
2431
+ }
2432
+
2433
+ state.modal.submitting = Boolean(submitting);
2434
+ if (submitting) {
2435
+ state.modal.error = null;
2436
+ }
2437
+ emitChange();
2438
+ }
2439
+
2440
+ export function setCopyColumnModalError(error) {
2441
+ if (state.modal?.kind !== 'copy-column') {
2442
+ return;
2443
+ }
2444
+
2445
+ withModalError(error);
2446
+ }
2447
+
2256
2448
  export function openEditConnectionModal(id) {
2257
2449
  const connection = state.connections.recent.find(entry => entry.id === id);
2258
2450
 
@@ -2845,7 +3037,7 @@ export async function executeCurrentQuery() {
2845
3037
  state.editor.result = response.data;
2846
3038
  resetEditorResultSort();
2847
3039
  state.editor.error = null;
2848
- invalidateDatabaseCaches();
3040
+ invalidateDatabaseCaches({ preserveDataBrowserState: true });
2849
3041
  await refreshQueryHistoryState();
2850
3042
  pushToast(response.message || `Executed ${response.data.statementCount} SQL statement(s).`, 'success');
2851
3043
  return true;
@@ -3143,6 +3335,26 @@ export function updateCurrentTableDesignerColumnField(columnId, field, value, op
3143
3335
  }
3144
3336
  }
3145
3337
 
3338
+ export function updateCurrentTableDesignerConstraintField(constraintKind, constraintId, field, value, options = {}) {
3339
+ if (!state.tableDesigner.draft) {
3340
+ return;
3341
+ }
3342
+
3343
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
3344
+ state.tableDesigner.draft,
3345
+ constraintKind,
3346
+ constraintId,
3347
+ field,
3348
+ value,
3349
+ getTableDesignerContext(),
3350
+ );
3351
+ state.tableDesigner.saveError = null;
3352
+
3353
+ if (options.notify !== false) {
3354
+ emitChange();
3355
+ }
3356
+ }
3357
+
3146
3358
  export function addCurrentTableDesignerColumn() {
3147
3359
  if (!state.tableDesigner.draft) {
3148
3360
  return null;
@@ -3158,9 +3370,15 @@ export function addCurrentTableDesignerColumn() {
3158
3370
  return nextColumn?.id ?? null;
3159
3371
  }
3160
3372
 
3161
- export function queueTableDesignerCsvImport(fileName, csvText) {
3373
+ export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
3162
3374
  try {
3163
- const imported = createTableDesignerDraftFromCsvImport({ fileName, csvText }, getTableDesignerContext());
3375
+ const imported = createTableDesignerDraftFromCsvImport(
3376
+ { fileName, csvText },
3377
+ {
3378
+ ...getTableDesignerContext(),
3379
+ ...(options.context ?? {}),
3380
+ },
3381
+ );
3164
3382
 
3165
3383
  state.tableDesigner.pendingImportedDraft = imported.draft;
3166
3384
  state.tableDesigner.selectedTableName = null;
@@ -3169,6 +3387,10 @@ export function queueTableDesignerCsvImport(fileName, csvText) {
3169
3387
  emitChange();
3170
3388
  return imported;
3171
3389
  } catch (error) {
3390
+ if (options.throwOnError) {
3391
+ throw error;
3392
+ }
3393
+
3172
3394
  pushToast(error?.message || 'CSV import failed.', 'alert');
3173
3395
  return null;
3174
3396
  }
@@ -3627,6 +3849,26 @@ export function openDataRowByIdentity(tableName, identity) {
3627
3849
  return true;
3628
3850
  }
3629
3851
 
3852
+ export function preserveCurrentDataRowSelectionForReload() {
3853
+ const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
3854
+ const row = getSelectedDataBrowserRow();
3855
+ const rowIndex =
3856
+ typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
3857
+
3858
+ if (!tableName || !row) {
3859
+ return false;
3860
+ }
3861
+
3862
+ state.dataBrowser.pendingOpenRow = {
3863
+ tableName,
3864
+ identity: row.__identity ?? null,
3865
+ rowIndex,
3866
+ };
3867
+ clearDataBrowserRowSelectionState();
3868
+ state.dataBrowser.saveError = null;
3869
+ return true;
3870
+ }
3871
+
3630
3872
  export function selectEditorRow(index) {
3631
3873
  const numericIndex = Number(index);
3632
3874
 
@@ -4132,25 +4374,138 @@ export async function submitDeleteQueryHistoryConfirmation() {
4132
4374
  return deleted;
4133
4375
  }
4134
4376
 
4135
- export async function exportCurrentQueryCsv() {
4377
+ function beginCurrentQueryExport() {
4136
4378
  state.editor.exportLoading = true;
4379
+ if (state.modal?.kind === 'query-export') {
4380
+ state.modal.submitting = true;
4381
+ state.modal.error = null;
4382
+ }
4137
4383
  emitChange();
4384
+ }
4385
+
4386
+ function reportCurrentQueryExportError(error) {
4387
+ if (state.modal?.kind === 'query-export') {
4388
+ withModalError(error);
4389
+ return;
4390
+ }
4391
+
4392
+ state.editor.error = normalizeError(error);
4393
+ emitChange();
4394
+ }
4395
+
4396
+ function finishCurrentQueryExport() {
4397
+ state.editor.exportLoading = false;
4398
+ if (state.modal?.kind === 'query-export') {
4399
+ state.modal.submitting = false;
4400
+ }
4401
+ emitChange();
4402
+ }
4403
+
4404
+ export async function exportCurrentQueryFormat(format = 'csv', filename = '') {
4405
+ const normalizedFormat = String(format ?? 'csv').toLowerCase();
4406
+ const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
4407
+ const exportFilename = getCurrentQueryExportFilename(normalizedFormat, filename || state.modal?.filename);
4408
+
4409
+ if (state.modal?.kind === 'query-export') {
4410
+ state.modal.filename = exportFilename;
4411
+ }
4412
+
4413
+ beginCurrentQueryExport();
4138
4414
 
4139
4415
  try {
4140
- await api.downloadQueryCsv(state.editor.sqlText);
4141
- pushToast('Query export started.', 'success');
4416
+ await api.downloadQueryExport(state.editor.sqlText, normalizedFormat, { filename: exportFilename });
4417
+ closeModalInternal();
4418
+ pushToast(`${label} export started.`, 'success');
4142
4419
  return true;
4143
4420
  } catch (error) {
4144
- state.editor.error = normalizeError(error);
4145
- emitChange();
4421
+ reportCurrentQueryExportError(error);
4146
4422
  return false;
4147
4423
  } finally {
4148
- state.editor.exportLoading = false;
4424
+ finishCurrentQueryExport();
4425
+ }
4426
+ }
4427
+
4428
+ export async function duplicateCurrentQueryAsTable(filename = '') {
4429
+ const exportFilename = getCurrentQueryExportFilename('csv', filename || state.modal?.filename);
4430
+
4431
+ if (state.modal?.kind === 'query-export') {
4432
+ state.modal.filename = exportFilename;
4433
+ }
4434
+
4435
+ beginCurrentQueryExport();
4436
+
4437
+ try {
4438
+ const response = await api.getQueryExport(state.editor.sqlText, 'csv');
4439
+ const exportData = response?.data ?? {};
4440
+ const imported = queueTableDesignerCsvImport(
4441
+ exportFilename || getCurrentQueryExportFilename('csv', exportData.filename),
4442
+ exportData.content || '',
4443
+ { throwOnError: true },
4444
+ );
4445
+
4446
+ closeModalInternal();
4447
+ pushToast(
4448
+ `Table draft created from ${imported.importedRowCount} row${imported.importedRowCount === 1 ? '' : 's'}.`,
4449
+ 'success',
4450
+ );
4451
+ return imported;
4452
+ } catch (error) {
4453
+ reportCurrentQueryExportError(error);
4454
+ return null;
4455
+ } finally {
4456
+ finishCurrentQueryExport();
4457
+ }
4458
+ }
4459
+
4460
+ export async function exportCurrentQueryCsv() {
4461
+ try {
4462
+ return await exportCurrentQueryFormat('csv');
4463
+ } catch (error) {
4464
+ state.editor.error = normalizeError(error);
4149
4465
  emitChange();
4466
+ return false;
4150
4467
  }
4151
4468
  }
4152
4469
 
4153
- export async function exportCurrentDataTableCsv() {
4470
+ function getCurrentDataTableExportOptions(format = 'csv') {
4471
+ return {
4472
+ sortColumn: state.dataBrowser.sortColumn,
4473
+ sortDirection: state.dataBrowser.sortDirection,
4474
+ filterColumn: state.dataBrowser.searchColumn,
4475
+ filterOperator: state.dataBrowser.filterOperator,
4476
+ filterValue: state.dataBrowser.searchQuery,
4477
+ format,
4478
+ };
4479
+ }
4480
+
4481
+ function beginCurrentDataTableExport() {
4482
+ state.dataBrowser.exportLoading = true;
4483
+ if (state.modal?.kind === 'data-export') {
4484
+ state.modal.submitting = true;
4485
+ state.modal.error = null;
4486
+ }
4487
+ emitChange();
4488
+ }
4489
+
4490
+ function reportCurrentDataTableExportError(error) {
4491
+ if (state.modal?.kind === 'data-export') {
4492
+ withModalError(error);
4493
+ return;
4494
+ }
4495
+
4496
+ state.dataBrowser.error = normalizeError(error);
4497
+ emitChange();
4498
+ }
4499
+
4500
+ function finishCurrentDataTableExport() {
4501
+ state.dataBrowser.exportLoading = false;
4502
+ if (state.modal?.kind === 'data-export') {
4503
+ state.modal.submitting = false;
4504
+ }
4505
+ emitChange();
4506
+ }
4507
+
4508
+ export async function exportCurrentDataTableFormat(format = 'csv', filename = '') {
4154
4509
  const tableName = state.dataBrowser.selectedTable;
4155
4510
 
4156
4511
  if (!tableName) {
@@ -4158,26 +4513,82 @@ export async function exportCurrentDataTableCsv() {
4158
4513
  return false;
4159
4514
  }
4160
4515
 
4161
- state.dataBrowser.exportLoading = true;
4162
- emitChange();
4516
+ const normalizedFormat = String(format ?? 'csv').toLowerCase();
4517
+ const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
4518
+ const exportFilename = getCurrentDataTableExportFilename(normalizedFormat, filename || state.modal?.filename);
4519
+
4520
+ if (state.modal?.kind === 'data-export') {
4521
+ state.modal.filename = exportFilename;
4522
+ }
4523
+
4524
+ beginCurrentDataTableExport();
4163
4525
 
4164
4526
  try {
4165
- await api.downloadTableCsv(tableName, {
4166
- sortColumn: state.dataBrowser.sortColumn,
4167
- sortDirection: state.dataBrowser.sortDirection,
4527
+ await api.downloadTableExport(tableName, {
4528
+ ...getCurrentDataTableExportOptions(normalizedFormat),
4529
+ filename: exportFilename,
4168
4530
  });
4169
- pushToast(`CSV export started for ${tableName}.`, 'success');
4531
+ closeModalInternal();
4532
+ pushToast(`${label} export started for ${tableName}.`, 'success');
4170
4533
  return true;
4171
4534
  } catch (error) {
4172
- state.dataBrowser.error = normalizeError(error);
4173
- emitChange();
4535
+ reportCurrentDataTableExportError(error);
4174
4536
  return false;
4175
4537
  } finally {
4176
- state.dataBrowser.exportLoading = false;
4177
- emitChange();
4538
+ finishCurrentDataTableExport();
4539
+ }
4540
+ }
4541
+
4542
+ export async function duplicateCurrentDataTableAsTable(filename = '') {
4543
+ const tableName = state.dataBrowser.selectedTable;
4544
+
4545
+ if (!tableName) {
4546
+ pushToast('No table selected for export.', 'alert');
4547
+ return null;
4548
+ }
4549
+
4550
+ const exportFilename = getCurrentDataTableExportFilename('csv', filename || state.modal?.filename);
4551
+
4552
+ if (state.modal?.kind === 'data-export') {
4553
+ state.modal.filename = exportFilename;
4554
+ }
4555
+
4556
+ beginCurrentDataTableExport();
4557
+
4558
+ try {
4559
+ const response = await api.getTableExport(tableName, getCurrentDataTableExportOptions('csv'));
4560
+ const exportData = response?.data ?? {};
4561
+ const imported = queueTableDesignerCsvImport(
4562
+ exportFilename || getCurrentDataTableExportFilename('csv', exportData.filename),
4563
+ exportData.content || '',
4564
+ {
4565
+ throwOnError: true,
4566
+ context: {
4567
+ catalogTables: state.tableDesigner.tables?.length
4568
+ ? state.tableDesigner.tables
4569
+ : state.dataBrowser.tables,
4570
+ },
4571
+ },
4572
+ );
4573
+
4574
+ closeModalInternal();
4575
+ pushToast(
4576
+ `Table draft created from ${imported.importedRowCount} row${imported.importedRowCount === 1 ? '' : 's'}.`,
4577
+ 'success',
4578
+ );
4579
+ return imported;
4580
+ } catch (error) {
4581
+ reportCurrentDataTableExportError(error);
4582
+ return null;
4583
+ } finally {
4584
+ finishCurrentDataTableExport();
4178
4585
  }
4179
4586
  }
4180
4587
 
4588
+ export async function exportCurrentDataTableCsv() {
4589
+ return exportCurrentDataTableFormat('csv');
4590
+ }
4591
+
4181
4592
  export function sortEditorResultsByColumn(columnName) {
4182
4593
  const normalizedColumn = String(columnName ?? '').trim();
4183
4594
  const result = state.editor.result;