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.
- package/README.md +118 -23
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +32 -2
- package/frontend/js/app.js +989 -13
- package/frontend/js/components/modal.js +644 -15
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +102 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +868 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +39 -6
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +476 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +3 -3
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +6 -0
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/storage/appStateStore.js +313 -0
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/cli-args.test.js +100 -0
- package/tests/copy-column-modal.test.js +83 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- package/fill.js +0 -526
package/frontend/js/store.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
5203
|
+
exportFilename || getCurrentDataTableExportFilename('csv', exportData.filename),
|
|
4345
5204
|
exportData.content || '',
|
|
4346
5205
|
{
|
|
4347
5206
|
throwOnError: true,
|