sqlite-hub 0.16.0 → 0.17.2

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 (53) hide show
  1. package/README.md +106 -19
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +34 -0
  5. package/frontend/js/app.js +481 -32
  6. package/frontend/js/components/modal.js +270 -50
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryHistoryPanel.js +126 -131
  9. package/frontend/js/components/queryResults.js +18 -1
  10. package/frontend/js/components/rowEditorPanel.js +42 -34
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +8 -0
  13. package/frontend/js/store.js +655 -2
  14. package/frontend/js/utils/jsonPreview.js +31 -0
  15. package/frontend/js/utils/markdownDocuments.js +248 -0
  16. package/frontend/js/utils/rowEditorValues.js +41 -0
  17. package/frontend/js/utils/tableScrollState.js +39 -0
  18. package/frontend/js/views/charts.js +8 -0
  19. package/frontend/js/views/data.js +4 -1
  20. package/frontend/js/views/documents.js +300 -0
  21. package/frontend/js/views/editor.js +2 -0
  22. package/frontend/js/views/settings.js +39 -3
  23. package/frontend/styles/components.css +19 -0
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +422 -0
  26. package/package.json +2 -1
  27. package/server/middleware/localRequestSecurity.js +84 -0
  28. package/server/routes/connections.js +18 -1
  29. package/server/routes/documents.js +163 -0
  30. package/server/routes/settings.js +22 -6
  31. package/server/server.js +18 -1
  32. package/server/services/nativeFileDialogService.js +168 -0
  33. package/server/services/sqlite/dataBrowserService.js +0 -4
  34. package/server/services/sqlite/exportService.js +5 -1
  35. package/server/services/sqlite/sqlExecutor.js +51 -2
  36. package/server/services/storage/appStateStore.js +313 -0
  37. package/server/utils/sqliteTypes.js +17 -8
  38. package/tests/cli-args.test.js +100 -0
  39. package/tests/connections-file-dialog-route.test.js +46 -0
  40. package/tests/copy-column-modal.test.js +97 -0
  41. package/tests/database-documents.test.js +85 -0
  42. package/tests/export-blob.test.js +46 -0
  43. package/tests/json-preview.test.js +49 -0
  44. package/tests/local-request-security.test.js +85 -0
  45. package/tests/markdown-documents.test.js +79 -0
  46. package/tests/native-file-dialog.test.js +78 -0
  47. package/tests/query-results-truncation.test.js +20 -0
  48. package/tests/row-editor-null-values.test.js +131 -0
  49. package/tests/settings-metadata.test.js +16 -0
  50. package/tests/settings-view.test.js +32 -0
  51. package/tests/sql-identifier-safety.test.js +9 -3
  52. package/tests/sql-result-limit.test.js +66 -0
  53. package/tests/table-scroll-state.test.js +70 -0
@@ -24,6 +24,7 @@ import {
24
24
  MEDIA_TAGGING_DEFAULT_TAG_TABLE,
25
25
  } from './lib/mediaTaggingDefaults.js';
26
26
  import { buildTextExportFilename } from './utils/exportFilenames.js';
27
+ import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
27
28
 
28
29
  const listeners = new Set();
29
30
  const DEFAULT_SETTINGS = {
@@ -50,6 +51,8 @@ const UI_PREFERENCE_STORAGE_KEYS = {
50
51
  chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
51
52
  chartsResultsVisible: 'sqlite_hub_charts_results_visible',
52
53
  tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
54
+ documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
55
+ documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
53
56
  };
54
57
  const QUERY_HISTORY_PAGE_SIZE = 30;
55
58
  const QUERY_HISTORY_RUN_LIMIT = 8;
@@ -73,6 +76,7 @@ let queryHistorySearchTimer = null;
73
76
  let chartsLoadVersion = 0;
74
77
  let chartsDetailLoadVersion = 0;
75
78
  let mediaTaggingPreviewVersion = 0;
79
+ let documentsLoadVersion = 0;
76
80
 
77
81
  function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
78
82
  try {
@@ -252,6 +256,7 @@ const state = {
252
256
  loading: false,
253
257
  error: null,
254
258
  appVersion: null,
259
+ sqliteVersion: null,
255
260
  },
256
261
  overview: {
257
262
  data: null,
@@ -333,6 +338,22 @@ const state = {
333
338
  resultLoading: false,
334
339
  resultError: null,
335
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
+ },
336
357
  tableDesigner: {
337
358
  tables: [],
338
359
  selectedTableName: null,
@@ -538,6 +559,7 @@ function requiresActiveDatabase(routeName) {
538
559
  'editor',
539
560
  'editorResults',
540
561
  'charts',
562
+ 'documents',
541
563
  'structure',
542
564
  'tableDesigner',
543
565
  'mediaTaggingSetup',
@@ -915,6 +937,74 @@ function resetChartsState() {
915
937
  state.charts.resultError = null;
916
938
  }
917
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
+
918
1008
  function normalizeChartsHeightPreset(value) {
919
1009
  const normalizedValue = String(value ?? '')
920
1010
  .trim()
@@ -1043,6 +1133,8 @@ function clearRouteSlices() {
1043
1133
  state.tableDesigner.saveError = null;
1044
1134
  state.structure.error = null;
1045
1135
  state.mediaTagging.error = null;
1136
+ state.documents.error = null;
1137
+ state.documents.saveError = null;
1046
1138
  }
1047
1139
 
1048
1140
  function setMissingDatabaseState() {
@@ -1126,6 +1218,9 @@ function setMissingDatabaseState() {
1126
1218
  resetChartsState();
1127
1219
  state.charts.error = error;
1128
1220
 
1221
+ resetDocumentsState();
1222
+ state.documents.error = error;
1223
+
1129
1224
  resetQueryHistoryState({ preserveSearch: false });
1130
1225
  }
1131
1226
 
@@ -1171,6 +1266,10 @@ function syncRouteContext() {
1171
1266
  state.tableDesigner.saveError = null;
1172
1267
  }
1173
1268
 
1269
+ if (route.name !== 'documents') {
1270
+ state.documents.saveError = null;
1271
+ }
1272
+
1174
1273
  if (route.name !== 'mediaTaggingSetup' && route.name !== 'mediaTaggingQueue') {
1175
1274
  state.mediaTagging.selectedTagKeys = [];
1176
1275
  }
@@ -1210,6 +1309,7 @@ async function refreshSettingsState() {
1210
1309
  ...(response.data ?? {}),
1211
1310
  };
1212
1311
  state.settings.appVersion = response.metadata?.appVersion ?? null;
1312
+ state.settings.sqliteVersion = response.metadata?.sqliteVersion ?? null;
1213
1313
  } catch (error) {
1214
1314
  state.settings.error = normalizeError(error);
1215
1315
  } finally {
@@ -1497,6 +1597,75 @@ async function loadCharts(version, route, options = {}) {
1497
1597
  await loadChartsDetail(resolveLoadableChartsHistoryId(route));
1498
1598
  }
1499
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
+
1500
1669
  async function loadOverview(version) {
1501
1670
  state.overview.loading = true;
1502
1671
  state.overview.error = null;
@@ -2082,6 +2251,7 @@ function invalidateDatabaseCaches(options = {}) {
2082
2251
  state.structure.detail = null;
2083
2252
  state.structure.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, true);
2084
2253
  state.structure.tableSearchQuery = '';
2254
+ resetDocumentsState();
2085
2255
  state.mediaTagging.loading = false;
2086
2256
  state.mediaTagging.previewLoading = false;
2087
2257
  state.mediaTagging.saving = false;
@@ -2148,6 +2318,9 @@ async function loadRouteData(route, options = {}) {
2148
2318
  case 'charts':
2149
2319
  await loadCharts(version, route, options);
2150
2320
  return;
2321
+ case 'documents':
2322
+ await loadDocuments(version, route);
2323
+ return;
2151
2324
  case 'editor':
2152
2325
  case 'editorResults':
2153
2326
  await refreshQueryHistoryState();
@@ -2381,6 +2554,7 @@ export function openCopyColumnModal({ scope = 'editor', columnName = '', mode =
2381
2554
  separator: preferences.separator,
2382
2555
  wrapper: preferences.wrapper,
2383
2556
  lineBreaks: preferences.lineBreaks,
2557
+ editedText: null,
2384
2558
  error: null,
2385
2559
  submitting: false,
2386
2560
  };
@@ -2425,6 +2599,14 @@ export function updateCopyColumnModalFormatField(field, value) {
2425
2599
  emitChange();
2426
2600
  }
2427
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
+
2428
2610
  export function setCopyColumnModalSubmitting(submitting) {
2429
2611
  if (state.modal?.kind !== 'copy-column') {
2430
2612
  return;
@@ -2445,6 +2627,445 @@ export function setCopyColumnModalError(error) {
2445
2627
  withModalError(error);
2446
2628
  }
2447
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
+
2448
3069
  export function openEditConnectionModal(id) {
2449
3070
  const connection = state.connections.recent.find(entry => entry.id === id);
2450
3071
 
@@ -2611,6 +3232,26 @@ export function openDeleteQueryHistoryModal(historyId) {
2611
3232
  emitChange();
2612
3233
  }
2613
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
+
2614
3255
  export function closeModal() {
2615
3256
  closeModalInternal();
2616
3257
  }
@@ -2849,6 +3490,16 @@ export async function submitCreateConnection(payload) {
2849
3490
  }
2850
3491
  }
2851
3492
 
3493
+ export async function chooseCreateDatabasePath() {
3494
+ try {
3495
+ const response = await api.chooseCreateDatabasePath();
3496
+ return response.data?.path ?? null;
3497
+ } catch (error) {
3498
+ pushToast(normalizeError(error).message || 'Native file dialog could not be opened.', 'alert');
3499
+ return null;
3500
+ }
3501
+ }
3502
+
2852
3503
  export async function submitImportSql(payload) {
2853
3504
  startModalSubmission();
2854
3505
 
@@ -4662,11 +5313,13 @@ export function getQueryMessages(snapshot = state) {
4662
5313
 
4663
5314
  return [
4664
5315
  ...snapshot.editor.result.statements.map(statement => ({
4665
- tone: statement.kind === 'resultSet' ? 'success' : inferStatusTone(statement.keyword),
5316
+ tone: statement.kind === 'resultSet' && statement.truncated ? 'warning' : statement.kind === 'resultSet' ? 'success' : inferStatusTone(statement.keyword),
4666
5317
  label: `${statement.keyword} #${statement.index + 1}`,
4667
5318
  value:
4668
5319
  statement.kind === 'resultSet'
4669
- ? `${statement.rowCount} row(s) returned.`
5320
+ ? statement.truncated
5321
+ ? `Showing the first ${statement.rowCount} row(s); the result was limited to ${statement.rowLimit}.`
5322
+ : `${statement.rowCount} row(s) returned.`
4670
5323
  : `${statement.changes} row(s) affected.`,
4671
5324
  })),
4672
5325
  ...queryMessages,