sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -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,8 @@ 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';
27
+ import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
25
28
 
26
29
  const listeners = new Set();
27
30
  const DEFAULT_SETTINGS = {
@@ -35,6 +38,9 @@ const DATA_FILTER_OPERATORS = new Set(['=', '!=', '<', '>', '<=', '>=', 'equals'
35
38
  const DATA_ROW_SIZE_STORAGE_KEY = 'data_row_size';
36
39
  const CHARTS_HISTORY_TAB_STORAGE_KEY = 'charts_history_tab';
37
40
  const QUERY_HISTORY_TAB_STORAGE_KEY = 'query_history_tab';
41
+ const COPY_COLUMN_SEPARATOR_STORAGE_KEY = 'sqlitehub.copyColumn.separator';
42
+ const COPY_COLUMN_WRAPPER_STORAGE_KEY = 'sqlitehub.copyColumn.wrapper';
43
+ const COPY_COLUMN_LINE_BREAKS_STORAGE_KEY = 'sqlitehub.copyColumn.lineBreaks';
38
44
  const UI_PREFERENCE_STORAGE_KEYS = {
39
45
  sqlEditorHistoryVisible: 'sqlite_hub_sql_editor_history_visible',
40
46
  sqlEditorEditorVisible: 'sqlite_hub_sql_editor_editor_visible',
@@ -45,11 +51,14 @@ const UI_PREFERENCE_STORAGE_KEYS = {
45
51
  chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
46
52
  chartsResultsVisible: 'sqlite_hub_charts_results_visible',
47
53
  tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
54
+ documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
55
+ documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
48
56
  };
49
57
  const QUERY_HISTORY_PAGE_SIZE = 30;
50
58
  const QUERY_HISTORY_RUN_LIMIT = 8;
51
59
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
52
60
  const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
61
+ const COPY_COLUMN_MODES = new Set(['column', 'column-with-header', 'first-10', 'markdown-todo']);
53
62
  const TEXT_EXPORT_FORMAT_LABELS = {
54
63
  csv: 'CSV',
55
64
  tsv: 'TSV',
@@ -67,6 +76,7 @@ let queryHistorySearchTimer = null;
67
76
  let chartsLoadVersion = 0;
68
77
  let chartsDetailLoadVersion = 0;
69
78
  let mediaTaggingPreviewVersion = 0;
79
+ let documentsLoadVersion = 0;
70
80
 
71
81
  function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
72
82
  try {
@@ -84,6 +94,34 @@ function storeDataPageSize(pageSize) {
84
94
  }
85
95
  }
86
96
 
97
+ function findQueryHistoryItemBySql(sql, snapshot = state) {
98
+ const normalizedSql = String(sql ?? '').trim();
99
+
100
+ if (!normalizedSql) {
101
+ return null;
102
+ }
103
+
104
+ if (String(snapshot.editor.historyDetail?.rawSql ?? '').trim() === normalizedSql) {
105
+ return snapshot.editor.historyDetail;
106
+ }
107
+
108
+ return snapshot.editor.history.find(entry => String(entry.rawSql ?? '').trim() === normalizedSql) ?? null;
109
+ }
110
+
111
+ function getCurrentQueryExportFilename(format = 'csv', filename = '') {
112
+ const queryText = String(state.editor.sqlText ?? '');
113
+ const historyItem = findQueryHistoryItemBySql(queryText);
114
+ const fallback = historyItem?.displayTitle || 'query-results';
115
+
116
+ return buildTextExportFilename(filename || fallback, { format, fallback });
117
+ }
118
+
119
+ function getCurrentDataTableExportFilename(format = 'csv', filename = '') {
120
+ const fallback = state.dataBrowser.selectedTable || 'table';
121
+
122
+ return buildTextExportFilename(filename || fallback, { format, fallback });
123
+ }
124
+
87
125
  function readStoredBoolean(key, fallback) {
88
126
  try {
89
127
  const value = globalThis.localStorage?.getItem(key);
@@ -127,6 +165,27 @@ function storeString(key, value) {
127
165
  }
128
166
  }
129
167
 
168
+ function readCopyColumnPreferences() {
169
+ return {
170
+ separator: readStoredString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, ','),
171
+ wrapper: readStoredString(COPY_COLUMN_WRAPPER_STORAGE_KEY, '"'),
172
+ lineBreaks: readStoredBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, false),
173
+ };
174
+ }
175
+
176
+ function normalizeCopyColumnMode(mode) {
177
+ const normalizedMode = String(mode ?? '').trim();
178
+ return COPY_COLUMN_MODES.has(normalizedMode) ? normalizedMode : 'column';
179
+ }
180
+
181
+ function normalizeCopyColumnScope(scope) {
182
+ return scope === 'charts' ? 'charts' : 'editor';
183
+ }
184
+
185
+ function getResultByCopyColumnScope(scope, snapshot = state) {
186
+ return normalizeCopyColumnScope(scope) === 'charts' ? snapshot.charts.result : snapshot.editor.result;
187
+ }
188
+
130
189
  function readStoredEditorActiveTab(fallback = 'messages') {
131
190
  try {
132
191
  const value = globalThis.localStorage?.getItem(UI_PREFERENCE_STORAGE_KEYS.sqlEditorActiveTab);
@@ -197,6 +256,7 @@ const state = {
197
256
  loading: false,
198
257
  error: null,
199
258
  appVersion: null,
259
+ sqliteVersion: null,
200
260
  },
201
261
  overview: {
202
262
  data: null,
@@ -278,6 +338,22 @@ const state = {
278
338
  resultLoading: false,
279
339
  resultError: null,
280
340
  },
341
+ documents: {
342
+ items: [],
343
+ selectedId: null,
344
+ selected: null,
345
+ draftFilename: '',
346
+ draftContent: '',
347
+ dirty: false,
348
+ editorVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, true),
349
+ previewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, true),
350
+ loading: false,
351
+ detailLoading: false,
352
+ saving: false,
353
+ deleting: false,
354
+ error: null,
355
+ saveError: null,
356
+ },
281
357
  tableDesigner: {
282
358
  tables: [],
283
359
  selectedTableName: null,
@@ -483,6 +559,7 @@ function requiresActiveDatabase(routeName) {
483
559
  'editor',
484
560
  'editorResults',
485
561
  'charts',
562
+ 'documents',
486
563
  'structure',
487
564
  'tableDesigner',
488
565
  'mediaTaggingSetup',
@@ -860,6 +937,74 @@ function resetChartsState() {
860
937
  state.charts.resultError = null;
861
938
  }
862
939
 
940
+ function resetDocumentsState() {
941
+ documentsLoadVersion += 1;
942
+ state.documents.items = [];
943
+ state.documents.selectedId = null;
944
+ state.documents.selected = null;
945
+ state.documents.draftFilename = '';
946
+ state.documents.draftContent = '';
947
+ state.documents.dirty = false;
948
+ state.documents.loading = false;
949
+ state.documents.detailLoading = false;
950
+ state.documents.saving = false;
951
+ state.documents.deleting = false;
952
+ state.documents.error = null;
953
+ state.documents.saveError = null;
954
+ }
955
+
956
+ function applyCurrentDocument(document) {
957
+ state.documents.selectedId = document?.id ?? null;
958
+ state.documents.selected = document ?? null;
959
+ state.documents.draftFilename = document?.filename ?? '';
960
+ state.documents.draftContent = document?.content ?? '';
961
+ state.documents.dirty = false;
962
+ state.documents.saveError = null;
963
+ }
964
+
965
+ function upsertDocumentSummary(document) {
966
+ if (!document?.id) {
967
+ return;
968
+ }
969
+
970
+ const summary = {
971
+ id: document.id,
972
+ databaseKey: document.databaseKey,
973
+ title: document.title,
974
+ filename: document.filename,
975
+ contentLength: document.contentLength,
976
+ createdAt: document.createdAt,
977
+ updatedAt: document.updatedAt,
978
+ };
979
+ const existingIndex = state.documents.items.findIndex(item => item.id === document.id);
980
+
981
+ if (existingIndex >= 0) {
982
+ state.documents.items.splice(existingIndex, 1, summary);
983
+ } else {
984
+ state.documents.items.unshift(summary);
985
+ }
986
+
987
+ state.documents.items.sort((left, right) => {
988
+ const leftTime = Date.parse(left.updatedAt ?? '') || 0;
989
+ const rightTime = Date.parse(right.updatedAt ?? '') || 0;
990
+ return rightTime - leftTime || String(left.id).localeCompare(String(right.id));
991
+ });
992
+ }
993
+
994
+ function resolveDocumentSelection(route) {
995
+ const requestedId = String(route.params?.documentId ?? '').trim();
996
+
997
+ if (requestedId && state.documents.items.some(item => String(item.id) === requestedId)) {
998
+ return requestedId;
999
+ }
1000
+
1001
+ if (state.documents.selectedId && state.documents.items.some(item => item.id === state.documents.selectedId)) {
1002
+ return state.documents.selectedId;
1003
+ }
1004
+
1005
+ return state.documents.items[0]?.id ?? null;
1006
+ }
1007
+
863
1008
  function normalizeChartsHeightPreset(value) {
864
1009
  const normalizedValue = String(value ?? '')
865
1010
  .trim()
@@ -988,6 +1133,8 @@ function clearRouteSlices() {
988
1133
  state.tableDesigner.saveError = null;
989
1134
  state.structure.error = null;
990
1135
  state.mediaTagging.error = null;
1136
+ state.documents.error = null;
1137
+ state.documents.saveError = null;
991
1138
  }
992
1139
 
993
1140
  function setMissingDatabaseState() {
@@ -1071,6 +1218,9 @@ function setMissingDatabaseState() {
1071
1218
  resetChartsState();
1072
1219
  state.charts.error = error;
1073
1220
 
1221
+ resetDocumentsState();
1222
+ state.documents.error = error;
1223
+
1074
1224
  resetQueryHistoryState({ preserveSearch: false });
1075
1225
  }
1076
1226
 
@@ -1116,6 +1266,10 @@ function syncRouteContext() {
1116
1266
  state.tableDesigner.saveError = null;
1117
1267
  }
1118
1268
 
1269
+ if (route.name !== 'documents') {
1270
+ state.documents.saveError = null;
1271
+ }
1272
+
1119
1273
  if (route.name !== 'mediaTaggingSetup' && route.name !== 'mediaTaggingQueue') {
1120
1274
  state.mediaTagging.selectedTagKeys = [];
1121
1275
  }
@@ -1155,6 +1309,7 @@ async function refreshSettingsState() {
1155
1309
  ...(response.data ?? {}),
1156
1310
  };
1157
1311
  state.settings.appVersion = response.metadata?.appVersion ?? null;
1312
+ state.settings.sqliteVersion = response.metadata?.sqliteVersion ?? null;
1158
1313
  } catch (error) {
1159
1314
  state.settings.error = normalizeError(error);
1160
1315
  } finally {
@@ -1442,6 +1597,75 @@ async function loadCharts(version, route, options = {}) {
1442
1597
  await loadChartsDetail(resolveLoadableChartsHistoryId(route));
1443
1598
  }
1444
1599
 
1600
+ async function loadDocumentDetail(version, documentId) {
1601
+ if (!documentId) {
1602
+ applyCurrentDocument(null);
1603
+ return;
1604
+ }
1605
+
1606
+ state.documents.detailLoading = true;
1607
+ state.documents.saveError = null;
1608
+ emitChange();
1609
+
1610
+ try {
1611
+ const response = await api.getDocument(documentId);
1612
+
1613
+ if (version !== routeLoadVersion) {
1614
+ return;
1615
+ }
1616
+
1617
+ const document = response.data ?? null;
1618
+ applyCurrentDocument(document);
1619
+ upsertDocumentSummary(document);
1620
+ } catch (error) {
1621
+ if (version !== routeLoadVersion) {
1622
+ return;
1623
+ }
1624
+
1625
+ applyCurrentDocument(null);
1626
+ state.documents.error = normalizeError(error);
1627
+ } finally {
1628
+ if (version === routeLoadVersion) {
1629
+ state.documents.detailLoading = false;
1630
+ emitChange();
1631
+ }
1632
+ }
1633
+ }
1634
+
1635
+ async function loadDocuments(version, route) {
1636
+ const requestVersion = ++documentsLoadVersion;
1637
+
1638
+ state.documents.loading = true;
1639
+ state.documents.error = null;
1640
+ emitChange();
1641
+
1642
+ try {
1643
+ const response = await api.getDocuments();
1644
+
1645
+ if (version !== routeLoadVersion || requestVersion !== documentsLoadVersion) {
1646
+ return;
1647
+ }
1648
+
1649
+ state.documents.items = response.data?.items ?? [];
1650
+ state.documents.error = null;
1651
+
1652
+ await loadDocumentDetail(version, resolveDocumentSelection(route));
1653
+ } catch (error) {
1654
+ if (version !== routeLoadVersion || requestVersion !== documentsLoadVersion) {
1655
+ return;
1656
+ }
1657
+
1658
+ state.documents.items = [];
1659
+ applyCurrentDocument(null);
1660
+ state.documents.error = normalizeError(error);
1661
+ } finally {
1662
+ if (version === routeLoadVersion && requestVersion === documentsLoadVersion) {
1663
+ state.documents.loading = false;
1664
+ emitChange();
1665
+ }
1666
+ }
1667
+ }
1668
+
1445
1669
  async function loadOverview(version) {
1446
1670
  state.overview.loading = true;
1447
1671
  state.overview.error = null;
@@ -1480,6 +1704,16 @@ async function resolvePendingDataBrowserRow(version) {
1480
1704
  return;
1481
1705
  }
1482
1706
 
1707
+ if (!pendingTarget.identity && Number.isInteger(pendingTarget.rowIndex)) {
1708
+ if (table.rows?.[pendingTarget.rowIndex]) {
1709
+ state.dataBrowser.selectedRowIndex = pendingTarget.rowIndex;
1710
+ state.dataBrowser.selectedRow = null;
1711
+ }
1712
+
1713
+ state.dataBrowser.pendingOpenRow = null;
1714
+ return;
1715
+ }
1716
+
1483
1717
  const matchingRowIndex = findDataBrowserRowIndexByIdentity(table.rows ?? [], pendingTarget.identity);
1484
1718
 
1485
1719
  if (matchingRowIndex >= 0) {
@@ -2017,6 +2251,7 @@ function invalidateDatabaseCaches(options = {}) {
2017
2251
  state.structure.detail = null;
2018
2252
  state.structure.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, true);
2019
2253
  state.structure.tableSearchQuery = '';
2254
+ resetDocumentsState();
2020
2255
  state.mediaTagging.loading = false;
2021
2256
  state.mediaTagging.previewLoading = false;
2022
2257
  state.mediaTagging.saving = false;
@@ -2083,6 +2318,9 @@ async function loadRouteData(route, options = {}) {
2083
2318
  case 'charts':
2084
2319
  await loadCharts(version, route, options);
2085
2320
  return;
2321
+ case 'documents':
2322
+ await loadDocuments(version, route);
2323
+ return;
2086
2324
  case 'editor':
2087
2325
  case 'editorResults':
2088
2326
  await refreshQueryHistoryState();
@@ -2255,11 +2493,12 @@ export async function setRoute(route) {
2255
2493
  await loadRouteData(route);
2256
2494
  }
2257
2495
 
2258
- export function openModal(kind) {
2496
+ export function openModal(kind, options = {}) {
2259
2497
  state.modal = {
2260
2498
  kind,
2261
2499
  error: null,
2262
2500
  submitting: false,
2501
+ ...options,
2263
2502
  };
2264
2503
  emitChange();
2265
2504
  }
@@ -2272,6 +2511,7 @@ export function openQueryExportModal() {
2272
2511
 
2273
2512
  state.modal = {
2274
2513
  kind: 'query-export',
2514
+ filename: getCurrentQueryExportFilename('csv'),
2275
2515
  error: null,
2276
2516
  submitting: false,
2277
2517
  };
@@ -2286,12 +2526,546 @@ export function openDataExportModal() {
2286
2526
 
2287
2527
  state.modal = {
2288
2528
  kind: 'data-export',
2529
+ filename: getCurrentDataTableExportFilename('csv'),
2289
2530
  error: null,
2290
2531
  submitting: false,
2291
2532
  };
2292
2533
  emitChange();
2293
2534
  }
2294
2535
 
2536
+ export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
2537
+ const resultScope = normalizeCopyColumnScope(scope);
2538
+ const normalizedColumnName = String(columnName ?? '');
2539
+ const result = getResultByCopyColumnScope(resultScope);
2540
+ const hasColumn = (result?.columns ?? []).some(column => String(column) === normalizedColumnName);
2541
+
2542
+ if (!hasColumn) {
2543
+ pushToast('Column could not be found in the current result set.', 'alert');
2544
+ return;
2545
+ }
2546
+
2547
+ const preferences = readCopyColumnPreferences();
2548
+
2549
+ state.modal = {
2550
+ kind: 'copy-column',
2551
+ scope: resultScope,
2552
+ columnName: normalizedColumnName,
2553
+ copyMode: normalizeCopyColumnMode(mode),
2554
+ separator: preferences.separator,
2555
+ wrapper: preferences.wrapper,
2556
+ lineBreaks: preferences.lineBreaks,
2557
+ editedText: null,
2558
+ error: null,
2559
+ submitting: false,
2560
+ };
2561
+ emitChange();
2562
+ }
2563
+
2564
+ export function storeCopyColumnPreferences({ separator = ',', wrapper = '"', lineBreaks = false } = {}) {
2565
+ storeString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, separator);
2566
+ storeString(COPY_COLUMN_WRAPPER_STORAGE_KEY, wrapper);
2567
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, lineBreaks);
2568
+
2569
+ if (state.modal?.kind === 'copy-column') {
2570
+ state.modal.separator = String(separator ?? '');
2571
+ state.modal.wrapper = String(wrapper ?? '');
2572
+ state.modal.lineBreaks = Boolean(lineBreaks);
2573
+ }
2574
+ }
2575
+
2576
+ export function updateCopyColumnModalFormatField(field, value) {
2577
+ if (state.modal?.kind !== 'copy-column') {
2578
+ return;
2579
+ }
2580
+
2581
+ const normalizedField = String(field ?? '').trim();
2582
+
2583
+ if (normalizedField !== 'separator' && normalizedField !== 'wrapper' && normalizedField !== 'lineBreaks') {
2584
+ return;
2585
+ }
2586
+
2587
+ const normalizedValue = normalizedField === 'lineBreaks' ? Boolean(value) : String(value ?? '');
2588
+ state.modal[normalizedField] = normalizedValue;
2589
+ state.modal.error = null;
2590
+
2591
+ if (normalizedField === 'lineBreaks') {
2592
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, normalizedValue);
2593
+ } else {
2594
+ storeString(
2595
+ normalizedField === 'separator' ? COPY_COLUMN_SEPARATOR_STORAGE_KEY : COPY_COLUMN_WRAPPER_STORAGE_KEY,
2596
+ normalizedValue,
2597
+ );
2598
+ }
2599
+ emitChange();
2600
+ }
2601
+
2602
+ export function setCopyColumnModalEditedText(text) {
2603
+ if (state.modal?.kind !== 'copy-column') {
2604
+ return;
2605
+ }
2606
+
2607
+ state.modal.editedText = String(text ?? '');
2608
+ }
2609
+
2610
+ export function setCopyColumnModalSubmitting(submitting) {
2611
+ if (state.modal?.kind !== 'copy-column') {
2612
+ return;
2613
+ }
2614
+
2615
+ state.modal.submitting = Boolean(submitting);
2616
+ if (submitting) {
2617
+ state.modal.error = null;
2618
+ }
2619
+ emitChange();
2620
+ }
2621
+
2622
+ export function setCopyColumnModalError(error) {
2623
+ if (state.modal?.kind !== 'copy-column') {
2624
+ return;
2625
+ }
2626
+
2627
+ withModalError(error);
2628
+ }
2629
+
2630
+ export function updateCurrentDocumentDraftField(field, value) {
2631
+ if (state.route.name !== 'documents') {
2632
+ return;
2633
+ }
2634
+
2635
+ const normalizedField = String(field ?? '').trim();
2636
+
2637
+ if (normalizedField === 'filename') {
2638
+ state.documents.draftFilename = String(value ?? '');
2639
+ } else if (normalizedField === 'content') {
2640
+ state.documents.draftContent = String(value ?? '');
2641
+ } else {
2642
+ return;
2643
+ }
2644
+
2645
+ state.documents.dirty = true;
2646
+ state.documents.saveError = null;
2647
+ emitChange();
2648
+ }
2649
+
2650
+ export function setDocumentsPaneVisibility(pane, visible) {
2651
+ const normalizedPane = String(pane ?? '').trim();
2652
+ const nextVisible = Boolean(visible);
2653
+
2654
+ if (normalizedPane !== 'editor' && normalizedPane !== 'preview') {
2655
+ return;
2656
+ }
2657
+
2658
+ if (normalizedPane === 'editor') {
2659
+ state.documents.editorVisible = nextVisible;
2660
+
2661
+ if (!state.documents.editorVisible && !state.documents.previewVisible) {
2662
+ state.documents.previewVisible = true;
2663
+ }
2664
+ } else {
2665
+ state.documents.previewVisible = nextVisible;
2666
+
2667
+ if (!state.documents.editorVisible && !state.documents.previewVisible) {
2668
+ state.documents.editorVisible = true;
2669
+ }
2670
+ }
2671
+
2672
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, state.documents.editorVisible);
2673
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, state.documents.previewVisible);
2674
+ emitChange();
2675
+ }
2676
+
2677
+ export function toggleDocumentsPane(pane) {
2678
+ const normalizedPane = String(pane ?? '').trim();
2679
+
2680
+ if (normalizedPane === 'editor') {
2681
+ setDocumentsPaneVisibility('editor', !state.documents.editorVisible);
2682
+ } else if (normalizedPane === 'preview') {
2683
+ setDocumentsPaneVisibility('preview', !state.documents.previewVisible);
2684
+ }
2685
+ }
2686
+
2687
+ function normalizeDocumentInsertionRange(range = null) {
2688
+ const contentLength = String(state.documents.draftContent ?? '').length;
2689
+ const start = Number(range?.start);
2690
+ const end = Number(range?.end);
2691
+ const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
2692
+ const normalizedEnd = Number.isInteger(end) ? Math.max(normalizedStart, Math.min(contentLength, end)) : normalizedStart;
2693
+
2694
+ return {
2695
+ start: normalizedStart,
2696
+ end: normalizedEnd,
2697
+ };
2698
+ }
2699
+
2700
+ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null) {
2701
+ const content = String(currentContent ?? '');
2702
+ const text = String(insertion ?? '').trim();
2703
+
2704
+ if (!text) {
2705
+ return content;
2706
+ }
2707
+
2708
+ const normalizedRange = normalizeDocumentInsertionRange(range);
2709
+ const before = content.slice(0, normalizedRange.start);
2710
+ const after = content.slice(normalizedRange.end);
2711
+ const prefix = before && !before.endsWith('\n\n') ? (before.endsWith('\n') ? '\n' : '\n\n') : '';
2712
+ const suffix = after && !after.startsWith('\n\n') ? (after.startsWith('\n') ? '\n' : '\n\n') : '';
2713
+
2714
+ return `${before}${prefix}${text}${suffix}${after}`;
2715
+ }
2716
+
2717
+ function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
2718
+ if (!state.documents.selectedId) {
2719
+ return false;
2720
+ }
2721
+
2722
+ const nextContent = buildDocumentMarkdownInsertion(state.documents.draftContent, markdown, range);
2723
+
2724
+ if (nextContent === state.documents.draftContent) {
2725
+ return false;
2726
+ }
2727
+
2728
+ state.documents.draftContent = nextContent;
2729
+ state.documents.dirty = true;
2730
+ state.documents.saveError = null;
2731
+ emitChange();
2732
+ return true;
2733
+ }
2734
+
2735
+ function getDocumentInsertQueryTitle(query) {
2736
+ return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
2737
+ }
2738
+
2739
+ async function openDocumentInsertModal(kind, insertionRange = null) {
2740
+ if (!state.documents.selectedId) {
2741
+ pushToast('Select a document before inserting Markdown.', 'alert');
2742
+ return;
2743
+ }
2744
+
2745
+ state.modal = {
2746
+ kind,
2747
+ documentId: state.documents.selectedId,
2748
+ insertionRange: normalizeDocumentInsertionRange(insertionRange),
2749
+ queries: [],
2750
+ selectedHistoryId: '',
2751
+ loading: true,
2752
+ error: null,
2753
+ submitting: false,
2754
+ };
2755
+ emitChange();
2756
+
2757
+ try {
2758
+ const response = await api.getQueryHistory({
2759
+ tab: 'saved',
2760
+ onlySaved: true,
2761
+ limit: 100,
2762
+ });
2763
+ const loadedQueries = response.data?.items ?? [];
2764
+ const queries =
2765
+ kind === 'document-insert-note'
2766
+ ? loadedQueries.filter(query => String(query.notes ?? '').trim())
2767
+ : loadedQueries;
2768
+
2769
+ if (state.modal?.kind !== kind) {
2770
+ return;
2771
+ }
2772
+
2773
+ state.modal.queries = queries;
2774
+ state.modal.selectedHistoryId = String(queries[0]?.id ?? '');
2775
+ state.modal.loading = false;
2776
+ state.modal.error = null;
2777
+ emitChange();
2778
+ } catch (error) {
2779
+ if (state.modal?.kind !== kind) {
2780
+ return;
2781
+ }
2782
+
2783
+ state.modal.loading = false;
2784
+ state.modal.error = normalizeError(error);
2785
+ emitChange();
2786
+ }
2787
+ }
2788
+
2789
+ export function updateDocumentInsertQuerySelection(historyId) {
2790
+ if (state.modal?.kind !== 'document-insert-table' && state.modal?.kind !== 'document-insert-note') {
2791
+ return;
2792
+ }
2793
+
2794
+ state.modal.selectedHistoryId = String(historyId ?? '');
2795
+ state.modal.error = null;
2796
+ emitChange();
2797
+ }
2798
+
2799
+ function getSelectedDocumentInsertQuery(modal) {
2800
+ const selectedHistoryId = String(modal?.selectedHistoryId ?? '');
2801
+
2802
+ return (modal?.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
2803
+ }
2804
+
2805
+ function setDocumentInsertModalError(message, code = 'DOCUMENT_INSERT_UNAVAILABLE') {
2806
+ if (!state.modal) {
2807
+ return;
2808
+ }
2809
+
2810
+ state.modal.error = { code, message };
2811
+ state.modal.submitting = false;
2812
+ emitChange();
2813
+ }
2814
+
2815
+ function canSubmitDocumentInsertModal(modal, kind) {
2816
+ if (modal?.kind !== kind) {
2817
+ return false;
2818
+ }
2819
+
2820
+ if (String(modal.documentId ?? '') !== String(state.documents.selectedId ?? '')) {
2821
+ setDocumentInsertModalError('The selected document changed while the dialog was open.');
2822
+ return false;
2823
+ }
2824
+
2825
+ return true;
2826
+ }
2827
+
2828
+ export async function openDocumentInsertTableModal(insertionRange = null) {
2829
+ await openDocumentInsertModal('document-insert-table', insertionRange);
2830
+ }
2831
+
2832
+ export async function openDocumentInsertNoteModal(insertionRange = null) {
2833
+ await openDocumentInsertModal('document-insert-note', insertionRange);
2834
+ }
2835
+
2836
+ export async function submitDocumentInsertTable() {
2837
+ const modal = state.modal;
2838
+
2839
+ if (!canSubmitDocumentInsertModal(modal, 'document-insert-table')) {
2840
+ return false;
2841
+ }
2842
+
2843
+ const query = getSelectedDocumentInsertQuery(modal);
2844
+
2845
+ if (!query) {
2846
+ setDocumentInsertModalError('Select a saved query before inserting a table.');
2847
+ return false;
2848
+ }
2849
+
2850
+ startModalSubmission();
2851
+
2852
+ try {
2853
+ const response = await api.getQueryExport(query.rawSql, 'md');
2854
+ const markdownTable = String(response.data?.content ?? '').trim();
2855
+
2856
+ if (!markdownTable) {
2857
+ throw new Error('The selected query returned no Markdown table content.');
2858
+ }
2859
+
2860
+ const inserted = insertMarkdownIntoCurrentDocumentDraft(markdownTable, modal.insertionRange);
2861
+
2862
+ closeModalInternal();
2863
+ if (inserted) {
2864
+ pushToast(`Inserted table from "${getDocumentInsertQueryTitle(query)}".`, 'success');
2865
+ }
2866
+ return inserted;
2867
+ } catch (error) {
2868
+ withModalError(error);
2869
+ return false;
2870
+ }
2871
+ }
2872
+
2873
+ export function submitDocumentInsertNote() {
2874
+ const modal = state.modal;
2875
+
2876
+ if (!canSubmitDocumentInsertModal(modal, 'document-insert-note')) {
2877
+ return false;
2878
+ }
2879
+
2880
+ const query = getSelectedDocumentInsertQuery(modal);
2881
+ const note = String(query?.notes ?? '').trim();
2882
+
2883
+ if (!query || !note) {
2884
+ setDocumentInsertModalError('Select a saved query with notes before inserting.');
2885
+ return false;
2886
+ }
2887
+
2888
+ startModalSubmission();
2889
+ const inserted = insertMarkdownIntoCurrentDocumentDraft(note, modal.insertionRange);
2890
+
2891
+ closeModalInternal();
2892
+ if (inserted) {
2893
+ pushToast(`Inserted note from "${getDocumentInsertQueryTitle(query)}".`, 'success');
2894
+ }
2895
+ return inserted;
2896
+ }
2897
+
2898
+ export async function createDocument(options = {}) {
2899
+ state.documents.saving = true;
2900
+ state.documents.saveError = null;
2901
+ emitChange();
2902
+
2903
+ try {
2904
+ const response = await api.createDocument({
2905
+ title: options.title,
2906
+ filename: options.filename,
2907
+ content: options.content,
2908
+ });
2909
+ const document = response.data ?? null;
2910
+
2911
+ if (document) {
2912
+ upsertDocumentSummary(document);
2913
+ applyCurrentDocument(document);
2914
+ }
2915
+
2916
+ if (options.toast !== false) {
2917
+ pushToast(`Document "${document?.filename ?? 'Untitled.md'}" created.`, 'success');
2918
+ }
2919
+
2920
+ return document;
2921
+ } catch (error) {
2922
+ state.documents.saveError = normalizeError(error);
2923
+ if (options.toast !== false) {
2924
+ pushToast(state.documents.saveError.message || 'Document could not be created.', 'alert');
2925
+ }
2926
+ return null;
2927
+ } finally {
2928
+ state.documents.saving = false;
2929
+ emitChange();
2930
+ }
2931
+ }
2932
+
2933
+ export async function saveCurrentDocument(options = {}) {
2934
+ const documentId = state.documents.selectedId;
2935
+
2936
+ if (!documentId) {
2937
+ return null;
2938
+ }
2939
+
2940
+ const submittedFilename = state.documents.draftFilename;
2941
+ const submittedContent = state.documents.draftContent;
2942
+
2943
+ state.documents.saving = true;
2944
+ state.documents.saveError = null;
2945
+ emitChange();
2946
+
2947
+ try {
2948
+ const response = await api.updateDocument(documentId, {
2949
+ filename: submittedFilename,
2950
+ content: submittedContent,
2951
+ });
2952
+ const document = response.data ?? null;
2953
+
2954
+ if (document) {
2955
+ const draftUnchanged =
2956
+ String(state.documents.selectedId ?? '') === String(documentId) &&
2957
+ state.documents.draftFilename === submittedFilename &&
2958
+ state.documents.draftContent === submittedContent;
2959
+
2960
+ upsertDocumentSummary(document);
2961
+ if (draftUnchanged) {
2962
+ applyCurrentDocument(document);
2963
+ }
2964
+ }
2965
+
2966
+ if (options.toast !== false) {
2967
+ pushToast(`Document "${document?.filename ?? 'document'}" saved.`, 'success');
2968
+ }
2969
+
2970
+ return document;
2971
+ } catch (error) {
2972
+ state.documents.saveError = normalizeError(error);
2973
+ if (options.toast !== false) {
2974
+ pushToast(state.documents.saveError.message || 'Document could not be saved.', 'alert');
2975
+ }
2976
+ return null;
2977
+ } finally {
2978
+ state.documents.saving = false;
2979
+ emitChange();
2980
+ }
2981
+ }
2982
+
2983
+ export async function deleteCurrentDocument(options = {}) {
2984
+ const documentId = options.documentId ?? state.documents.selectedId;
2985
+ const reportErrorToModal = Boolean(options.reportErrorToModal);
2986
+
2987
+ if (!documentId) {
2988
+ return { deleted: false, nextDocumentId: null };
2989
+ }
2990
+
2991
+ const deletedIndex = state.documents.items.findIndex(item => item.id === documentId);
2992
+ const isSelectedDocument = String(state.documents.selectedId ?? '') === String(documentId);
2993
+
2994
+ state.documents.deleting = true;
2995
+ state.documents.saveError = null;
2996
+ emitChange();
2997
+
2998
+ try {
2999
+ await api.deleteDocument(documentId);
3000
+ state.documents.items = state.documents.items.filter(item => item.id !== documentId);
3001
+ const nextDocument = state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
3002
+ if (isSelectedDocument) {
3003
+ applyCurrentDocument(null);
3004
+ }
3005
+ pushToast('Document deleted.', 'success');
3006
+ return { deleted: true, nextDocumentId: nextDocument?.id ?? null };
3007
+ } catch (error) {
3008
+ state.documents.saveError = normalizeError(error);
3009
+ if (reportErrorToModal) {
3010
+ withModalError(error);
3011
+ } else {
3012
+ pushToast(state.documents.saveError.message || 'Document could not be deleted.', 'alert');
3013
+ }
3014
+ return { deleted: false, nextDocumentId: null };
3015
+ } finally {
3016
+ state.documents.deleting = false;
3017
+ emitChange();
3018
+ }
3019
+ }
3020
+
3021
+ export async function toggleCurrentDocumentTodo(lineIndex) {
3022
+ if (!state.documents.selectedId) {
3023
+ return null;
3024
+ }
3025
+
3026
+ const nextContent = toggleMarkdownTodoLine(state.documents.draftContent, lineIndex);
3027
+
3028
+ if (nextContent === state.documents.draftContent) {
3029
+ return state.documents.selected;
3030
+ }
3031
+
3032
+ state.documents.draftContent = nextContent;
3033
+ state.documents.dirty = true;
3034
+ state.documents.saveError = null;
3035
+ emitChange();
3036
+
3037
+ return saveCurrentDocument({ toast: false });
3038
+ }
3039
+
3040
+ export async function createDocumentFromMarkdownExport({ filename, content, title } = {}) {
3041
+ return createDocument({
3042
+ title,
3043
+ filename,
3044
+ content,
3045
+ toast: false,
3046
+ });
3047
+ }
3048
+
3049
+ export async function submitDeleteDocumentConfirmation() {
3050
+ const modal = state.modal;
3051
+
3052
+ if (modal?.kind !== 'delete-document') {
3053
+ return { deleted: false, nextDocumentId: null };
3054
+ }
3055
+
3056
+ startModalSubmission();
3057
+ const result = await deleteCurrentDocument({
3058
+ documentId: modal.documentId,
3059
+ reportErrorToModal: true,
3060
+ });
3061
+
3062
+ if (result.deleted) {
3063
+ closeModalInternal();
3064
+ }
3065
+
3066
+ return result;
3067
+ }
3068
+
2295
3069
  export function openEditConnectionModal(id) {
2296
3070
  const connection = state.connections.recent.find(entry => entry.id === id);
2297
3071
 
@@ -2458,6 +3232,26 @@ export function openDeleteQueryHistoryModal(historyId) {
2458
3232
  emitChange();
2459
3233
  }
2460
3234
 
3235
+ export function openDeleteDocumentModal() {
3236
+ const documentId = state.documents.selectedId;
3237
+ const document = state.documents.selected;
3238
+
3239
+ if (!documentId || !document) {
3240
+ pushToast('The selected document could not be loaded.', 'alert');
3241
+ return;
3242
+ }
3243
+
3244
+ state.modal = {
3245
+ kind: 'delete-document',
3246
+ documentId,
3247
+ filename: document.filename,
3248
+ contentLength: document.contentLength ?? String(document.content ?? '').length,
3249
+ error: null,
3250
+ submitting: false,
3251
+ };
3252
+ emitChange();
3253
+ }
3254
+
2461
3255
  export function closeModal() {
2462
3256
  closeModalInternal();
2463
3257
  }
@@ -3182,6 +3976,26 @@ export function updateCurrentTableDesignerColumnField(columnId, field, value, op
3182
3976
  }
3183
3977
  }
3184
3978
 
3979
+ export function updateCurrentTableDesignerConstraintField(constraintKind, constraintId, field, value, options = {}) {
3980
+ if (!state.tableDesigner.draft) {
3981
+ return;
3982
+ }
3983
+
3984
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
3985
+ state.tableDesigner.draft,
3986
+ constraintKind,
3987
+ constraintId,
3988
+ field,
3989
+ value,
3990
+ getTableDesignerContext(),
3991
+ );
3992
+ state.tableDesigner.saveError = null;
3993
+
3994
+ if (options.notify !== false) {
3995
+ emitChange();
3996
+ }
3997
+ }
3998
+
3185
3999
  export function addCurrentTableDesignerColumn() {
3186
4000
  if (!state.tableDesigner.draft) {
3187
4001
  return null;
@@ -3676,6 +4490,26 @@ export function openDataRowByIdentity(tableName, identity) {
3676
4490
  return true;
3677
4491
  }
3678
4492
 
4493
+ export function preserveCurrentDataRowSelectionForReload() {
4494
+ const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
4495
+ const row = getSelectedDataBrowserRow();
4496
+ const rowIndex =
4497
+ typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
4498
+
4499
+ if (!tableName || !row) {
4500
+ return false;
4501
+ }
4502
+
4503
+ state.dataBrowser.pendingOpenRow = {
4504
+ tableName,
4505
+ identity: row.__identity ?? null,
4506
+ rowIndex,
4507
+ };
4508
+ clearDataBrowserRowSelectionState();
4509
+ state.dataBrowser.saveError = null;
4510
+ return true;
4511
+ }
4512
+
3679
4513
  export function selectEditorRow(index) {
3680
4514
  const numericIndex = Number(index);
3681
4515
 
@@ -4208,14 +5042,19 @@ function finishCurrentQueryExport() {
4208
5042
  emitChange();
4209
5043
  }
4210
5044
 
4211
- export async function exportCurrentQueryFormat(format = 'csv') {
5045
+ export async function exportCurrentQueryFormat(format = 'csv', filename = '') {
4212
5046
  const normalizedFormat = String(format ?? 'csv').toLowerCase();
4213
5047
  const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
5048
+ const exportFilename = getCurrentQueryExportFilename(normalizedFormat, filename || state.modal?.filename);
5049
+
5050
+ if (state.modal?.kind === 'query-export') {
5051
+ state.modal.filename = exportFilename;
5052
+ }
4214
5053
 
4215
5054
  beginCurrentQueryExport();
4216
5055
 
4217
5056
  try {
4218
- await api.downloadQueryExport(state.editor.sqlText, normalizedFormat);
5057
+ await api.downloadQueryExport(state.editor.sqlText, normalizedFormat, { filename: exportFilename });
4219
5058
  closeModalInternal();
4220
5059
  pushToast(`${label} export started.`, 'success');
4221
5060
  return true;
@@ -4227,14 +5066,20 @@ export async function exportCurrentQueryFormat(format = 'csv') {
4227
5066
  }
4228
5067
  }
4229
5068
 
4230
- export async function duplicateCurrentQueryAsTable() {
5069
+ export async function duplicateCurrentQueryAsTable(filename = '') {
5070
+ const exportFilename = getCurrentQueryExportFilename('csv', filename || state.modal?.filename);
5071
+
5072
+ if (state.modal?.kind === 'query-export') {
5073
+ state.modal.filename = exportFilename;
5074
+ }
5075
+
4231
5076
  beginCurrentQueryExport();
4232
5077
 
4233
5078
  try {
4234
5079
  const response = await api.getQueryExport(state.editor.sqlText, 'csv');
4235
5080
  const exportData = response?.data ?? {};
4236
5081
  const imported = queueTableDesignerCsvImport(
4237
- exportData.filename || 'query-results.csv',
5082
+ exportFilename || getCurrentQueryExportFilename('csv', exportData.filename),
4238
5083
  exportData.content || '',
4239
5084
  { throwOnError: true },
4240
5085
  );
@@ -4301,7 +5146,7 @@ function finishCurrentDataTableExport() {
4301
5146
  emitChange();
4302
5147
  }
4303
5148
 
4304
- export async function exportCurrentDataTableFormat(format = 'csv') {
5149
+ export async function exportCurrentDataTableFormat(format = 'csv', filename = '') {
4305
5150
  const tableName = state.dataBrowser.selectedTable;
4306
5151
 
4307
5152
  if (!tableName) {
@@ -4311,11 +5156,19 @@ export async function exportCurrentDataTableFormat(format = 'csv') {
4311
5156
 
4312
5157
  const normalizedFormat = String(format ?? 'csv').toLowerCase();
4313
5158
  const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
5159
+ const exportFilename = getCurrentDataTableExportFilename(normalizedFormat, filename || state.modal?.filename);
5160
+
5161
+ if (state.modal?.kind === 'data-export') {
5162
+ state.modal.filename = exportFilename;
5163
+ }
4314
5164
 
4315
5165
  beginCurrentDataTableExport();
4316
5166
 
4317
5167
  try {
4318
- await api.downloadTableExport(tableName, getCurrentDataTableExportOptions(normalizedFormat));
5168
+ await api.downloadTableExport(tableName, {
5169
+ ...getCurrentDataTableExportOptions(normalizedFormat),
5170
+ filename: exportFilename,
5171
+ });
4319
5172
  closeModalInternal();
4320
5173
  pushToast(`${label} export started for ${tableName}.`, 'success');
4321
5174
  return true;
@@ -4327,7 +5180,7 @@ export async function exportCurrentDataTableFormat(format = 'csv') {
4327
5180
  }
4328
5181
  }
4329
5182
 
4330
- export async function duplicateCurrentDataTableAsTable() {
5183
+ export async function duplicateCurrentDataTableAsTable(filename = '') {
4331
5184
  const tableName = state.dataBrowser.selectedTable;
4332
5185
 
4333
5186
  if (!tableName) {
@@ -4335,13 +5188,19 @@ export async function duplicateCurrentDataTableAsTable() {
4335
5188
  return null;
4336
5189
  }
4337
5190
 
5191
+ const exportFilename = getCurrentDataTableExportFilename('csv', filename || state.modal?.filename);
5192
+
5193
+ if (state.modal?.kind === 'data-export') {
5194
+ state.modal.filename = exportFilename;
5195
+ }
5196
+
4338
5197
  beginCurrentDataTableExport();
4339
5198
 
4340
5199
  try {
4341
5200
  const response = await api.getTableExport(tableName, getCurrentDataTableExportOptions('csv'));
4342
5201
  const exportData = response?.data ?? {};
4343
5202
  const imported = queueTableDesignerCsvImport(
4344
- exportData.filename || `${tableName}.csv`,
5203
+ exportFilename || getCurrentDataTableExportFilename('csv', exportData.filename),
4345
5204
  exportData.content || '',
4346
5205
  {
4347
5206
  throwOnError: true,