sqlite-hub 1.1.0 → 1.1.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 +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +140 -10
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +29 -18
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +1 -0
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/structureGraph.js +1 -0
- package/frontend/js/components/tableDesignerEditor.js +23 -42
- package/frontend/js/components/tableDesignerSidebar.js +7 -31
- package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +407 -38
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +176 -0
- package/frontend/js/views/charts.js +12 -11
- package/frontend/js/views/data.js +37 -35
- package/frontend/js/views/documents.js +231 -162
- package/frontend/js/views/mediaTagging.js +6 -5
- package/frontend/js/views/structure.js +47 -43
- package/frontend/js/views/tableDesigner.js +45 -1
- package/frontend/styles/components.css +237 -59
- package/frontend/styles/structure-graph.css +10 -2
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +84 -41
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/dataBrowserService.js +11 -3
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +64 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +38 -3
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +75 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +24 -0
- package/tests/structure-view.test.js +56 -0
package/frontend/js/store.js
CHANGED
|
@@ -19,12 +19,10 @@ import {
|
|
|
19
19
|
suggestQueryChartType,
|
|
20
20
|
validateQueryChartConfig,
|
|
21
21
|
} from './lib/queryCharts.js';
|
|
22
|
-
import {
|
|
23
|
-
MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
|
|
24
|
-
MEDIA_TAGGING_DEFAULT_TAG_TABLE,
|
|
25
|
-
} from './lib/mediaTaggingDefaults.js';
|
|
22
|
+
import { MEDIA_TAGGING_DEFAULT_MAPPING_TABLE, MEDIA_TAGGING_DEFAULT_TAG_TABLE } from './lib/mediaTaggingDefaults.js';
|
|
26
23
|
import { buildTextExportFilename } from './utils/exportFilenames.js';
|
|
27
24
|
import { toggleMarkdownTodoLine } from './utils/markdownDocuments.js';
|
|
25
|
+
import { buildRiskySqlBackupContext, detectRiskySqlOperations } from './utils/riskySql.js';
|
|
28
26
|
|
|
29
27
|
const listeners = new Set();
|
|
30
28
|
const DEFAULT_SETTINGS = {
|
|
@@ -51,9 +49,13 @@ const UI_PREFERENCE_STORAGE_KEYS = {
|
|
|
51
49
|
chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
|
|
52
50
|
chartsQueryVisible: 'sqlite_hub_charts_query_visible',
|
|
53
51
|
chartsResultsVisible: 'sqlite_hub_charts_results_visible',
|
|
52
|
+
chartsHeightPreset: 'sqlite_hub_charts_height_preset',
|
|
53
|
+
tableDesignerTablesVisible: 'sqlite_hub_table_designer_tables_visible',
|
|
54
54
|
tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
|
|
55
|
+
documentsVisible: 'sqlite_hub_documents_visible',
|
|
55
56
|
documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
|
|
56
57
|
documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
|
|
58
|
+
mediaTaggingViewerVisible: 'sqlite_hub_media_tagging_viewer_visible',
|
|
57
59
|
};
|
|
58
60
|
const QUERY_HISTORY_PAGE_SIZE = 30;
|
|
59
61
|
const QUERY_HISTORY_RUN_LIMIT = 8;
|
|
@@ -226,6 +228,15 @@ function storeChartsHistoryTab(tab) {
|
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
function readStoredChartsHeightPreset(fallback = 'medium') {
|
|
232
|
+
return normalizeChartsHeightPreset(readStoredString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, fallback));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function storeChartsHeightPreset(preset) {
|
|
236
|
+
const normalizedPreset = normalizeChartsHeightPreset(preset);
|
|
237
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.chartsHeightPreset, normalizedPreset);
|
|
238
|
+
}
|
|
239
|
+
|
|
229
240
|
function readStoredQueryHistoryTab(fallback = 'recent') {
|
|
230
241
|
try {
|
|
231
242
|
return normalizeQueryHistoryTab(globalThis.localStorage?.getItem(QUERY_HISTORY_TAB_STORAGE_KEY) ?? fallback);
|
|
@@ -254,6 +265,12 @@ const state = {
|
|
|
254
265
|
backupLoading: false,
|
|
255
266
|
error: null,
|
|
256
267
|
},
|
|
268
|
+
backups: {
|
|
269
|
+
items: [],
|
|
270
|
+
loading: false,
|
|
271
|
+
operationLoading: false,
|
|
272
|
+
error: null,
|
|
273
|
+
},
|
|
257
274
|
settings: {
|
|
258
275
|
data: { ...DEFAULT_SETTINGS },
|
|
259
276
|
section: 'information',
|
|
@@ -342,7 +359,7 @@ const state = {
|
|
|
342
359
|
historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
|
|
343
360
|
detailPanelVisible: false,
|
|
344
361
|
selectedHistoryId: null,
|
|
345
|
-
chartHeightPreset:
|
|
362
|
+
chartHeightPreset: readStoredChartsHeightPreset(),
|
|
346
363
|
queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
|
|
347
364
|
resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
|
|
348
365
|
detail: null,
|
|
@@ -356,9 +373,11 @@ const state = {
|
|
|
356
373
|
items: [],
|
|
357
374
|
selectedId: null,
|
|
358
375
|
selected: null,
|
|
376
|
+
searchQuery: '',
|
|
359
377
|
draftFilename: '',
|
|
360
378
|
draftContent: '',
|
|
361
379
|
dirty: false,
|
|
380
|
+
documentsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, true),
|
|
362
381
|
editorVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsEditorVisible, true),
|
|
363
382
|
previewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsPreviewVisible, true),
|
|
364
383
|
loading: false,
|
|
@@ -372,6 +391,7 @@ const state = {
|
|
|
372
391
|
tables: [],
|
|
373
392
|
selectedTableName: null,
|
|
374
393
|
draft: null,
|
|
394
|
+
tablesVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true),
|
|
375
395
|
sqlPreviewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true),
|
|
376
396
|
pendingImportedDraft: null,
|
|
377
397
|
loading: false,
|
|
@@ -424,7 +444,7 @@ const state = {
|
|
|
424
444
|
issues: [],
|
|
425
445
|
dismissedIssueKeys: [],
|
|
426
446
|
selectedTagKeys: [],
|
|
427
|
-
workflowMediaDetailsVisible: true,
|
|
447
|
+
workflowMediaDetailsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible, true),
|
|
428
448
|
workflowMediaRotationDegrees: 0,
|
|
429
449
|
skippedMediaKeys: [],
|
|
430
450
|
tagFormValues: {},
|
|
@@ -569,6 +589,7 @@ function clearQueryHistoryDetailState() {
|
|
|
569
589
|
function requiresActiveDatabase(routeName) {
|
|
570
590
|
return [
|
|
571
591
|
'overview',
|
|
592
|
+
'backups',
|
|
572
593
|
'data',
|
|
573
594
|
'editor',
|
|
574
595
|
'editorResults',
|
|
@@ -607,7 +628,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
|
|
|
607
628
|
return 0;
|
|
608
629
|
}
|
|
609
630
|
|
|
610
|
-
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
631
|
+
return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
|
|
611
632
|
}
|
|
612
633
|
|
|
613
634
|
function normalizeDataPageSize(value, fallback = 50) {
|
|
@@ -814,8 +835,10 @@ function areRowIdentitiesEqual(left, right) {
|
|
|
814
835
|
return false;
|
|
815
836
|
}
|
|
816
837
|
|
|
817
|
-
return
|
|
818
|
-
|
|
838
|
+
return (
|
|
839
|
+
JSON.stringify(left.columns ?? []) === JSON.stringify(right.columns ?? []) &&
|
|
840
|
+
JSON.stringify(left.values ?? null) === JSON.stringify(right.values ?? null)
|
|
841
|
+
);
|
|
819
842
|
}
|
|
820
843
|
|
|
821
844
|
function getSelectedDataBrowserRow(snapshot = state) {
|
|
@@ -847,9 +870,7 @@ function clearDataBrowserRowSelectionState() {
|
|
|
847
870
|
|
|
848
871
|
function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
849
872
|
const hasRowIndex =
|
|
850
|
-
rowIndex !== null &&
|
|
851
|
-
rowIndex !== undefined &&
|
|
852
|
-
(typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
873
|
+
rowIndex !== null && rowIndex !== undefined && (typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
853
874
|
const numericIndex = hasRowIndex ? Number(rowIndex) : NaN;
|
|
854
875
|
|
|
855
876
|
if (Number.isInteger(numericIndex) && numericIndex >= 0) {
|
|
@@ -867,7 +888,11 @@ function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
|
867
888
|
const selectedRow = getSelectedDataBrowserRow();
|
|
868
889
|
const selectedIdentity = identity ?? selectedRow?.__identity ?? null;
|
|
869
890
|
|
|
870
|
-
if (
|
|
891
|
+
if (
|
|
892
|
+
!selectedRow?.__identity ||
|
|
893
|
+
!selectedIdentity ||
|
|
894
|
+
!areRowIdentitiesEqual(selectedRow.__identity, selectedIdentity)
|
|
895
|
+
) {
|
|
871
896
|
return {
|
|
872
897
|
row: null,
|
|
873
898
|
rowIndex: null,
|
|
@@ -943,7 +968,7 @@ function resetChartsState() {
|
|
|
943
968
|
state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
|
|
944
969
|
state.charts.detailPanelVisible = false;
|
|
945
970
|
state.charts.selectedHistoryId = null;
|
|
946
|
-
state.charts.chartHeightPreset =
|
|
971
|
+
state.charts.chartHeightPreset = readStoredChartsHeightPreset(state.charts.chartHeightPreset);
|
|
947
972
|
state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
|
|
948
973
|
state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
|
|
949
974
|
state.charts.detail = null;
|
|
@@ -959,6 +984,7 @@ function resetDocumentsState() {
|
|
|
959
984
|
state.documents.items = [];
|
|
960
985
|
state.documents.selectedId = null;
|
|
961
986
|
state.documents.selected = null;
|
|
987
|
+
state.documents.searchQuery = '';
|
|
962
988
|
state.documents.draftFilename = '';
|
|
963
989
|
state.documents.draftContent = '';
|
|
964
990
|
state.documents.dirty = false;
|
|
@@ -1061,7 +1087,9 @@ function syncQueryHistoryItem(updatedItem) {
|
|
|
1061
1087
|
|
|
1062
1088
|
const updatedItemId = String(updatedItem.id);
|
|
1063
1089
|
|
|
1064
|
-
state.editor.history = state.editor.history.map(entry =>
|
|
1090
|
+
state.editor.history = state.editor.history.map(entry =>
|
|
1091
|
+
String(entry.id) === updatedItemId ? updatedItem : entry,
|
|
1092
|
+
);
|
|
1065
1093
|
|
|
1066
1094
|
if (String(state.editor.historyDetail?.id ?? '') === updatedItemId) {
|
|
1067
1095
|
state.editor.historyDetail = updatedItem;
|
|
@@ -1189,7 +1217,11 @@ function setMissingDatabaseState() {
|
|
|
1189
1217
|
state.tableDesigner.tables = [];
|
|
1190
1218
|
state.tableDesigner.selectedTableName = null;
|
|
1191
1219
|
state.tableDesigner.draft = null;
|
|
1192
|
-
state.tableDesigner.
|
|
1220
|
+
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
1221
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
1222
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
1223
|
+
true,
|
|
1224
|
+
);
|
|
1193
1225
|
state.tableDesigner.pendingImportedDraft = null;
|
|
1194
1226
|
state.tableDesigner.saving = false;
|
|
1195
1227
|
state.tableDesigner.searchQuery = '';
|
|
@@ -1338,6 +1370,30 @@ async function refreshSettingsState() {
|
|
|
1338
1370
|
}
|
|
1339
1371
|
}
|
|
1340
1372
|
|
|
1373
|
+
async function refreshBackupsState() {
|
|
1374
|
+
if (!state.connections.active) {
|
|
1375
|
+
state.backups.items = [];
|
|
1376
|
+
state.backups.error = null;
|
|
1377
|
+
emitChange();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
state.backups.loading = true;
|
|
1382
|
+
state.backups.error = null;
|
|
1383
|
+
emitChange();
|
|
1384
|
+
|
|
1385
|
+
try {
|
|
1386
|
+
const response = await api.getBackups();
|
|
1387
|
+
state.backups.items = response.data ?? [];
|
|
1388
|
+
state.backups.error = null;
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
state.backups.error = normalizeError(error);
|
|
1391
|
+
} finally {
|
|
1392
|
+
state.backups.loading = false;
|
|
1393
|
+
emitChange();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1341
1397
|
export async function createSettingsApiToken(name) {
|
|
1342
1398
|
state.settings.tokenSaving = true;
|
|
1343
1399
|
state.settings.error = null;
|
|
@@ -1381,9 +1437,7 @@ export async function checkSettingsAppVersion() {
|
|
|
1381
1437
|
|
|
1382
1438
|
state.settings.versionCheck = result;
|
|
1383
1439
|
pushToast(
|
|
1384
|
-
result?.updateAvailable
|
|
1385
|
-
? `SQLite Hub v${result.latestVersion} is available.`
|
|
1386
|
-
: 'SQLite Hub is up to date.',
|
|
1440
|
+
result?.updateAvailable ? `SQLite Hub v${result.latestVersion} is available.` : 'SQLite Hub is up to date.',
|
|
1387
1441
|
'success',
|
|
1388
1442
|
);
|
|
1389
1443
|
return result;
|
|
@@ -1665,11 +1719,17 @@ function getRequestedChartsHistoryId(route) {
|
|
|
1665
1719
|
function resolveLoadableChartsHistoryId(route) {
|
|
1666
1720
|
const requestedHistoryId = getRequestedChartsHistoryId(route);
|
|
1667
1721
|
|
|
1668
|
-
if (
|
|
1669
|
-
return null;
|
|
1722
|
+
if (requestedHistoryId) {
|
|
1723
|
+
return state.charts.queries.some(item => item.id === requestedHistoryId) ? requestedHistoryId : null;
|
|
1670
1724
|
}
|
|
1671
1725
|
|
|
1672
|
-
|
|
1726
|
+
const selectedHistoryId = Number(state.charts.selectedHistoryId);
|
|
1727
|
+
|
|
1728
|
+
return Number.isInteger(selectedHistoryId) &&
|
|
1729
|
+
selectedHistoryId > 0 &&
|
|
1730
|
+
state.charts.queries.some(item => item.id === selectedHistoryId)
|
|
1731
|
+
? selectedHistoryId
|
|
1732
|
+
: null;
|
|
1673
1733
|
}
|
|
1674
1734
|
|
|
1675
1735
|
function hasSettledChartsDetail(historyId) {
|
|
@@ -2384,7 +2444,11 @@ function invalidateDatabaseCaches(options = {}) {
|
|
|
2384
2444
|
state.tableDesigner.tables = [];
|
|
2385
2445
|
state.tableDesigner.selectedTableName = null;
|
|
2386
2446
|
state.tableDesigner.draft = null;
|
|
2387
|
-
state.tableDesigner.
|
|
2447
|
+
state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
|
|
2448
|
+
state.tableDesigner.sqlPreviewVisible = readStoredBoolean(
|
|
2449
|
+
UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible,
|
|
2450
|
+
true,
|
|
2451
|
+
);
|
|
2388
2452
|
state.tableDesigner.pendingImportedDraft = null;
|
|
2389
2453
|
state.tableDesigner.saving = false;
|
|
2390
2454
|
state.tableDesigner.searchQuery = '';
|
|
@@ -2428,6 +2492,10 @@ function invalidateDatabaseCaches(options = {}) {
|
|
|
2428
2492
|
state.mediaTagging.issues = [];
|
|
2429
2493
|
state.mediaTagging.dismissedIssueKeys = [];
|
|
2430
2494
|
state.mediaTagging.selectedTagKeys = [];
|
|
2495
|
+
state.mediaTagging.workflowMediaDetailsVisible = readStoredBoolean(
|
|
2496
|
+
UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible,
|
|
2497
|
+
true,
|
|
2498
|
+
);
|
|
2431
2499
|
state.mediaTagging.workflowMediaRotationDegrees = 0;
|
|
2432
2500
|
state.mediaTagging.skippedMediaKeys = [];
|
|
2433
2501
|
state.mediaTagging.tagFormValues = {};
|
|
@@ -2457,6 +2525,9 @@ async function loadRouteData(route, options = {}) {
|
|
|
2457
2525
|
case 'overview':
|
|
2458
2526
|
await loadOverview(version);
|
|
2459
2527
|
return;
|
|
2528
|
+
case 'backups':
|
|
2529
|
+
await refreshBackupsState();
|
|
2530
|
+
return;
|
|
2460
2531
|
case 'data':
|
|
2461
2532
|
await loadData(version, route);
|
|
2462
2533
|
return;
|
|
@@ -2494,7 +2565,7 @@ function pushToast(message, tone = 'muted') {
|
|
|
2494
2565
|
|
|
2495
2566
|
window.setTimeout(() => {
|
|
2496
2567
|
dismissToast(id);
|
|
2497
|
-
},
|
|
2568
|
+
}, 4500);
|
|
2498
2569
|
}
|
|
2499
2570
|
|
|
2500
2571
|
function withModalError(error) {
|
|
@@ -2834,7 +2905,9 @@ function normalizeDocumentInsertionRange(range = null) {
|
|
|
2834
2905
|
const start = Number(range?.start);
|
|
2835
2906
|
const end = Number(range?.end);
|
|
2836
2907
|
const normalizedStart = Number.isInteger(start) ? Math.max(0, Math.min(contentLength, start)) : contentLength;
|
|
2837
|
-
const normalizedEnd = Number.isInteger(end)
|
|
2908
|
+
const normalizedEnd = Number.isInteger(end)
|
|
2909
|
+
? Math.max(normalizedStart, Math.min(contentLength, end))
|
|
2910
|
+
: normalizedStart;
|
|
2838
2911
|
|
|
2839
2912
|
return {
|
|
2840
2913
|
start: normalizedStart,
|
|
@@ -3143,7 +3216,8 @@ export async function deleteCurrentDocument(options = {}) {
|
|
|
3143
3216
|
try {
|
|
3144
3217
|
await api.deleteDocument(documentId);
|
|
3145
3218
|
state.documents.items = state.documents.items.filter(item => item.id !== documentId);
|
|
3146
|
-
const nextDocument =
|
|
3219
|
+
const nextDocument =
|
|
3220
|
+
state.documents.items[Math.max(0, Math.min(deletedIndex, state.documents.items.length - 1))] ?? null;
|
|
3147
3221
|
if (isSelectedDocument) {
|
|
3148
3222
|
applyCurrentDocument(null);
|
|
3149
3223
|
}
|
|
@@ -3233,9 +3307,7 @@ export function openDeleteDataRowModal(rowIndex) {
|
|
|
3233
3307
|
const tableName = state.dataBrowser.selectedTable;
|
|
3234
3308
|
const numericIndex = Number(rowIndex);
|
|
3235
3309
|
const hasNumericIndex = Number.isInteger(numericIndex) && numericIndex >= 0;
|
|
3236
|
-
const row = hasNumericIndex
|
|
3237
|
-
? state.dataBrowser.table?.rows?.[numericIndex] ?? null
|
|
3238
|
-
: getSelectedDataBrowserRow();
|
|
3310
|
+
const row = hasNumericIndex ? (state.dataBrowser.table?.rows?.[numericIndex] ?? null) : getSelectedDataBrowserRow();
|
|
3239
3311
|
const rowPreview = buildDeleteRowPreview(
|
|
3240
3312
|
(state.dataBrowser.table?.columnMeta ?? [])
|
|
3241
3313
|
.filter(column => column.visible)
|
|
@@ -3530,6 +3602,7 @@ export function setChartsHeightPreset(preset) {
|
|
|
3530
3602
|
}
|
|
3531
3603
|
|
|
3532
3604
|
state.charts.chartHeightPreset = nextPreset;
|
|
3605
|
+
storeChartsHeightPreset(nextPreset);
|
|
3533
3606
|
emitChange();
|
|
3534
3607
|
}
|
|
3535
3608
|
|
|
@@ -3680,10 +3753,36 @@ export async function chooseCreateDatabasePath() {
|
|
|
3680
3753
|
}
|
|
3681
3754
|
|
|
3682
3755
|
export async function submitImportSql(payload) {
|
|
3756
|
+
if (!payload?.skipSafety && !payload?.createNew && !payload?.targetConnectionId && !payload?.targetPath) {
|
|
3757
|
+
try {
|
|
3758
|
+
const preview = await api.previewImportSql(payload);
|
|
3759
|
+
|
|
3760
|
+
if (preview.data?.requiresSafetyBackup) {
|
|
3761
|
+
state.modal = {
|
|
3762
|
+
kind: 'backup-safety',
|
|
3763
|
+
operation: 'import',
|
|
3764
|
+
importPayload: payload,
|
|
3765
|
+
backupType: 'pre_import',
|
|
3766
|
+
backupName: 'Before import',
|
|
3767
|
+
notes: `SQL import, ${preview.data.statementCount ?? 0} statements, ${preview.data.sizeBytes ?? 0} bytes`,
|
|
3768
|
+
description: 'This SQL dump is large enough to change substantial data. Creating a backup allows you to restore the current state if the import goes wrong.',
|
|
3769
|
+
error: null,
|
|
3770
|
+
submitting: false,
|
|
3771
|
+
};
|
|
3772
|
+
emitChange();
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
withModalError(error);
|
|
3777
|
+
return null;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3683
3781
|
startModalSubmission();
|
|
3684
3782
|
|
|
3685
3783
|
try {
|
|
3686
|
-
const
|
|
3784
|
+
const { skipSafety, ...requestPayload } = payload ?? {};
|
|
3785
|
+
const response = await api.importSql(requestPayload);
|
|
3687
3786
|
closeModalInternal();
|
|
3688
3787
|
pushToast(response.message || 'SQL dump imported.', 'success');
|
|
3689
3788
|
await refreshConnectionsState();
|
|
@@ -3759,28 +3858,257 @@ export async function removeConnection(id) {
|
|
|
3759
3858
|
}
|
|
3760
3859
|
|
|
3761
3860
|
export async function createActiveConnectionBackup() {
|
|
3861
|
+
openCreateBackupModal();
|
|
3862
|
+
return null;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
export function openCreateBackupModal(defaults = {}) {
|
|
3762
3866
|
if (!state.connections.active) {
|
|
3763
3867
|
pushToast('No active SQLite database selected for backup.', 'alert');
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
state.modal = {
|
|
3872
|
+
kind: 'create-backup',
|
|
3873
|
+
name: defaults.name ?? '',
|
|
3874
|
+
notes: defaults.notes ?? '',
|
|
3875
|
+
backupType: defaults.type ?? 'manual',
|
|
3876
|
+
error: null,
|
|
3877
|
+
submitting: false,
|
|
3878
|
+
};
|
|
3879
|
+
emitChange();
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
export async function submitCreateBackupConfirmation({ name = '', notes = '', type = 'manual' } = {}) {
|
|
3883
|
+
if (state.modal?.kind !== 'create-backup') {
|
|
3764
3884
|
return null;
|
|
3765
3885
|
}
|
|
3766
3886
|
|
|
3887
|
+
startModalSubmission();
|
|
3767
3888
|
state.connections.backupLoading = true;
|
|
3768
|
-
|
|
3889
|
+
state.backups.operationLoading = true;
|
|
3769
3890
|
|
|
3770
3891
|
try {
|
|
3771
|
-
const response = await api.
|
|
3772
|
-
|
|
3892
|
+
const response = await api.createBackup({ name, notes, type });
|
|
3893
|
+
state.backups.items = [
|
|
3894
|
+
response.data,
|
|
3895
|
+
...state.backups.items.filter(item => item.id !== response.data.id),
|
|
3896
|
+
];
|
|
3897
|
+
closeModalInternal();
|
|
3773
3898
|
pushToast(response.message || 'Backup created.', 'success');
|
|
3899
|
+
if (state.route.name === 'backups') {
|
|
3900
|
+
await refreshBackupsState();
|
|
3901
|
+
}
|
|
3774
3902
|
return response.data;
|
|
3775
3903
|
} catch (error) {
|
|
3776
|
-
|
|
3904
|
+
withModalError(error);
|
|
3777
3905
|
return null;
|
|
3778
3906
|
} finally {
|
|
3779
3907
|
state.connections.backupLoading = false;
|
|
3908
|
+
state.backups.operationLoading = false;
|
|
3780
3909
|
emitChange();
|
|
3781
3910
|
}
|
|
3782
3911
|
}
|
|
3783
3912
|
|
|
3913
|
+
export function openEditBackupNotesModal(backupId) {
|
|
3914
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3915
|
+
|
|
3916
|
+
if (!backup) {
|
|
3917
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
state.modal = {
|
|
3922
|
+
kind: 'edit-backup-notes',
|
|
3923
|
+
backupId: backup.id,
|
|
3924
|
+
backupName: backup.name,
|
|
3925
|
+
notes: backup.notes ?? '',
|
|
3926
|
+
error: null,
|
|
3927
|
+
submitting: false,
|
|
3928
|
+
};
|
|
3929
|
+
emitChange();
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
export async function submitEditBackupNotesConfirmation(notes = '') {
|
|
3933
|
+
if (state.modal?.kind !== 'edit-backup-notes') {
|
|
3934
|
+
return null;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const backupId = state.modal.backupId;
|
|
3938
|
+
startModalSubmission();
|
|
3939
|
+
state.backups.operationLoading = true;
|
|
3940
|
+
|
|
3941
|
+
try {
|
|
3942
|
+
const response = await api.updateBackup(backupId, { notes });
|
|
3943
|
+
state.backups.items = state.backups.items.map(item =>
|
|
3944
|
+
String(item.id) === String(response.data.id) ? response.data : item
|
|
3945
|
+
);
|
|
3946
|
+
closeModalInternal();
|
|
3947
|
+
pushToast(response.message || 'Backup notes updated.', 'success');
|
|
3948
|
+
return response.data;
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
withModalError(error);
|
|
3951
|
+
return null;
|
|
3952
|
+
} finally {
|
|
3953
|
+
state.backups.operationLoading = false;
|
|
3954
|
+
emitChange();
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
export function openDeleteBackupModal(backupId) {
|
|
3959
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
3960
|
+
|
|
3961
|
+
if (!backup) {
|
|
3962
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
state.modal = {
|
|
3967
|
+
kind: 'delete-backup',
|
|
3968
|
+
backupId: backup.id,
|
|
3969
|
+
backupName: backup.name,
|
|
3970
|
+
fileName: backup.fileName,
|
|
3971
|
+
error: null,
|
|
3972
|
+
submitting: false,
|
|
3973
|
+
};
|
|
3974
|
+
emitChange();
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
export async function submitDeleteBackupConfirmation() {
|
|
3978
|
+
if (state.modal?.kind !== 'delete-backup') {
|
|
3979
|
+
return false;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
const backupId = state.modal.backupId;
|
|
3983
|
+
startModalSubmission();
|
|
3984
|
+
state.backups.operationLoading = true;
|
|
3985
|
+
|
|
3986
|
+
try {
|
|
3987
|
+
const response = await api.deleteBackup(backupId);
|
|
3988
|
+
state.backups.items = state.backups.items.filter(item => String(item.id) !== String(backupId));
|
|
3989
|
+
closeModalInternal();
|
|
3990
|
+
pushToast(response.message || 'Backup deleted.', 'muted');
|
|
3991
|
+
return true;
|
|
3992
|
+
} catch (error) {
|
|
3993
|
+
withModalError(error);
|
|
3994
|
+
return false;
|
|
3995
|
+
} finally {
|
|
3996
|
+
state.backups.operationLoading = false;
|
|
3997
|
+
emitChange();
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
export async function downloadBackup(backupId) {
|
|
4002
|
+
state.backups.operationLoading = true;
|
|
4003
|
+
emitChange();
|
|
4004
|
+
|
|
4005
|
+
try {
|
|
4006
|
+
await api.downloadBackup(backupId);
|
|
4007
|
+
pushToast('Backup download started.', 'success');
|
|
4008
|
+
return true;
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
state.backups.error = normalizeError(error);
|
|
4011
|
+
pushToast(state.backups.error.message || 'Backup could not be downloaded.', 'alert');
|
|
4012
|
+
return false;
|
|
4013
|
+
} finally {
|
|
4014
|
+
state.backups.operationLoading = false;
|
|
4015
|
+
emitChange();
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
export function openRestoreBackupModal(backupId) {
|
|
4020
|
+
const backup = state.backups.items.find(item => String(item.id) === String(backupId));
|
|
4021
|
+
|
|
4022
|
+
if (!backup) {
|
|
4023
|
+
pushToast('The selected backup could not be loaded.', 'alert');
|
|
4024
|
+
return;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
state.modal = {
|
|
4028
|
+
kind: 'backup-safety',
|
|
4029
|
+
operation: 'restore',
|
|
4030
|
+
backupId: backup.id,
|
|
4031
|
+
backupName: backup.name,
|
|
4032
|
+
backupType: 'pre_restore',
|
|
4033
|
+
backupNameBeforeOperation: `Before restore of ${backup.name}`,
|
|
4034
|
+
description: 'Restoring replaces the current database file with this backup.',
|
|
4035
|
+
error: null,
|
|
4036
|
+
submitting: false,
|
|
4037
|
+
};
|
|
4038
|
+
emitChange();
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
async function restoreBackupById(backupId) {
|
|
4042
|
+
state.backups.operationLoading = true;
|
|
4043
|
+
|
|
4044
|
+
try {
|
|
4045
|
+
const response = await api.restoreBackup(backupId);
|
|
4046
|
+
pushToast(response.message || 'Backup restored.', 'success');
|
|
4047
|
+
await Promise.all([refreshConnectionsState(), refreshBackupsState()]);
|
|
4048
|
+
invalidateDatabaseCaches();
|
|
4049
|
+
return response.data;
|
|
4050
|
+
} finally {
|
|
4051
|
+
state.backups.operationLoading = false;
|
|
4052
|
+
emitChange();
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
async function createSafetyBackupFromModal(modal) {
|
|
4057
|
+
const response = await api.createBackup({
|
|
4058
|
+
name: modal.backupNameBeforeOperation ?? modal.backupName,
|
|
4059
|
+
notes: modal.notes,
|
|
4060
|
+
type: modal.backupType,
|
|
4061
|
+
});
|
|
4062
|
+
|
|
4063
|
+
if (state.route.name === 'backups') {
|
|
4064
|
+
await refreshBackupsState();
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return response.data;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
export async function submitBackupSafetyChoice(choice) {
|
|
4071
|
+
const modal = state.modal;
|
|
4072
|
+
|
|
4073
|
+
if (modal?.kind !== 'backup-safety') {
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
if (choice === 'cancel') {
|
|
4078
|
+
closeModalInternal();
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
startModalSubmission();
|
|
4083
|
+
|
|
4084
|
+
try {
|
|
4085
|
+
if (choice === 'create') {
|
|
4086
|
+
await createSafetyBackupFromModal(modal);
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
if (modal.operation === 'restore') {
|
|
4090
|
+
await restoreBackupById(modal.backupId);
|
|
4091
|
+
} else if (modal.operation === 'sql') {
|
|
4092
|
+
await executeCurrentQuery({ skipSafety: true });
|
|
4093
|
+
} else if (modal.operation === 'import') {
|
|
4094
|
+
await submitImportSql({
|
|
4095
|
+
...(modal.importPayload ?? {}),
|
|
4096
|
+
skipSafety: true,
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
closeModalInternal();
|
|
4101
|
+
return true;
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
withModalError(error);
|
|
4104
|
+
return false;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
export async function refreshBackups() {
|
|
4109
|
+
await refreshBackupsState();
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3784
4112
|
export async function openOverviewInFinder() {
|
|
3785
4113
|
try {
|
|
3786
4114
|
const response = await api.openOverviewInFinder();
|
|
@@ -3853,7 +4181,26 @@ export function setEditorTab(tab) {
|
|
|
3853
4181
|
emitChange();
|
|
3854
4182
|
}
|
|
3855
4183
|
|
|
3856
|
-
export async function executeCurrentQuery() {
|
|
4184
|
+
export async function executeCurrentQuery(options = {}) {
|
|
4185
|
+
const riskyOperations = detectRiskySqlOperations(state.editor.sqlText);
|
|
4186
|
+
|
|
4187
|
+
if (!options.skipSafety && riskyOperations.length) {
|
|
4188
|
+
const context = buildRiskySqlBackupContext(riskyOperations);
|
|
4189
|
+
state.modal = {
|
|
4190
|
+
kind: 'backup-safety',
|
|
4191
|
+
operation: 'sql',
|
|
4192
|
+
backupType: context.type,
|
|
4193
|
+
backupName: context.name,
|
|
4194
|
+
notes: state.editor.sqlText.slice(0, 600),
|
|
4195
|
+
description: context.description,
|
|
4196
|
+
sqlText: state.editor.sqlText,
|
|
4197
|
+
error: null,
|
|
4198
|
+
submitting: false,
|
|
4199
|
+
};
|
|
4200
|
+
emitChange();
|
|
4201
|
+
return false;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
3857
4204
|
state.editor.executing = true;
|
|
3858
4205
|
state.editor.lastExecutedSql = state.editor.sqlText;
|
|
3859
4206
|
state.editor.error = null;
|
|
@@ -4108,6 +4455,12 @@ export function setTableDesignerSearchQuery(query) {
|
|
|
4108
4455
|
emitChange();
|
|
4109
4456
|
}
|
|
4110
4457
|
|
|
4458
|
+
export function toggleTableDesignerTablesPanel() {
|
|
4459
|
+
state.tableDesigner.tablesVisible = state.tableDesigner.tablesVisible === false;
|
|
4460
|
+
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, state.tableDesigner.tablesVisible);
|
|
4461
|
+
emitChange();
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4111
4464
|
export function setDataTableSearchQuery(query) {
|
|
4112
4465
|
state.dataBrowser.tableSearchQuery = String(query ?? '');
|
|
4113
4466
|
emitChange();
|
|
@@ -4118,6 +4471,17 @@ export function setStructureTableSearchQuery(query) {
|
|
|
4118
4471
|
emitChange();
|
|
4119
4472
|
}
|
|
4120
4473
|
|
|
4474
|
+
export function setDocumentsSearchQuery(query) {
|
|
4475
|
+
state.documents.searchQuery = String(query ?? '');
|
|
4476
|
+
emitChange();
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
export function toggleDocumentsPanel() {
|
|
4480
|
+
state.documents.documentsVisible = state.documents.documentsVisible === false;
|
|
4481
|
+
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.documentsVisible, state.documents.documentsVisible);
|
|
4482
|
+
emitChange();
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4121
4485
|
export function setTableDesignerSqlPreviewVisibility(visible) {
|
|
4122
4486
|
const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
|
|
4123
4487
|
|
|
@@ -4359,6 +4723,7 @@ export function setMediaTaggingWorkflowMediaDetailsVisible(value, options = {})
|
|
|
4359
4723
|
}
|
|
4360
4724
|
|
|
4361
4725
|
state.mediaTagging.workflowMediaDetailsVisible = nextValue;
|
|
4726
|
+
storeBoolean(UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible, nextValue);
|
|
4362
4727
|
|
|
4363
4728
|
if (options.notify !== false) {
|
|
4364
4729
|
emitChange();
|
|
@@ -4684,8 +5049,7 @@ export function openDataRowByIdentity(tableName, identity) {
|
|
|
4684
5049
|
export function preserveCurrentDataRowSelectionForReload() {
|
|
4685
5050
|
const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
|
|
4686
5051
|
const row = getSelectedDataBrowserRow();
|
|
4687
|
-
const rowIndex =
|
|
4688
|
-
typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
5052
|
+
const rowIndex = typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
|
|
4689
5053
|
|
|
4690
5054
|
if (!tableName || !row) {
|
|
4691
5055
|
return false;
|
|
@@ -5494,7 +5858,12 @@ export function getQueryMessages(snapshot = state) {
|
|
|
5494
5858
|
|
|
5495
5859
|
return [
|
|
5496
5860
|
...snapshot.editor.result.statements.map(statement => ({
|
|
5497
|
-
tone:
|
|
5861
|
+
tone:
|
|
5862
|
+
statement.kind === 'resultSet' && statement.truncated
|
|
5863
|
+
? 'warning'
|
|
5864
|
+
: statement.kind === 'resultSet'
|
|
5865
|
+
? 'success'
|
|
5866
|
+
: inferStatusTone(statement.keyword),
|
|
5498
5867
|
label: `${statement.keyword} #${statement.index + 1}`,
|
|
5499
5868
|
value:
|
|
5500
5869
|
statement.kind === 'resultSet'
|