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.
- package/README.md +106 -19
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +481 -32
- package/frontend/js/components/modal.js +270 -50
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +18 -1
- package/frontend/js/components/rowEditorPanel.js +42 -34
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +655 -2
- package/frontend/js/utils/jsonPreview.js +31 -0
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/utils/rowEditorValues.js +41 -0
- package/frontend/js/utils/tableScrollState.js +39 -0
- package/frontend/js/views/charts.js +8 -0
- package/frontend/js/views/data.js +4 -1
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +39 -3
- package/frontend/styles/components.css +19 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +2 -1
- package/server/middleware/localRequestSecurity.js +84 -0
- package/server/routes/connections.js +18 -1
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +18 -1
- package/server/services/nativeFileDialogService.js +168 -0
- package/server/services/sqlite/dataBrowserService.js +0 -4
- package/server/services/sqlite/exportService.js +5 -1
- package/server/services/sqlite/sqlExecutor.js +51 -2
- package/server/services/storage/appStateStore.js +313 -0
- package/server/utils/sqliteTypes.js +17 -8
- package/tests/cli-args.test.js +100 -0
- package/tests/connections-file-dialog-route.test.js +46 -0
- package/tests/copy-column-modal.test.js +97 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-blob.test.js +46 -0
- package/tests/json-preview.test.js +49 -0
- package/tests/local-request-security.test.js +85 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/native-file-dialog.test.js +78 -0
- package/tests/query-results-truncation.test.js +20 -0
- package/tests/row-editor-null-values.test.js +131 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
- package/tests/sql-identifier-safety.test.js +9 -3
- package/tests/sql-result-limit.test.js +66 -0
- package/tests/table-scroll-state.test.js +70 -0
package/frontend/js/store.js
CHANGED
|
@@ -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
|
-
?
|
|
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,
|