sqlite-hub 0.9.13 → 0.10.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/frontend/js/api.js +7 -0
- package/frontend/js/app.js +20 -4
- package/frontend/js/components/modal.js +98 -1
- package/frontend/js/components/queryEditor.js +1 -8
- package/frontend/js/store.js +176 -10
- package/frontend/js/views/data.js +39 -1
- package/frontend/js/views/editor.js +1 -3
- package/frontend/js/views/structure.js +43 -5
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +4 -0
- package/package.json +1 -1
- package/server/routes/data.js +14 -0
- package/server/services/sqlite/dataBrowserService.js +124 -44
- package/tests/sql-identifier-safety.test.js +48 -8
package/frontend/js/api.js
CHANGED
|
@@ -385,6 +385,13 @@ export function updateDataTableRow(tableName, payload) {
|
|
|
385
385
|
});
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
export function previewDataTableRowUpdate(tableName, payload) {
|
|
389
|
+
return request(`/api/data/${encodeURIComponent(tableName)}/rows/preview-update`, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
body: payload,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
388
395
|
export function deleteDataTableRow(tableName, payload) {
|
|
389
396
|
return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
|
|
390
397
|
method: "DELETE",
|
package/frontend/js/app.js
CHANGED
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
openEditConnectionModal,
|
|
50
50
|
openCreateQueryChartModal,
|
|
51
51
|
openEditQueryChartModal,
|
|
52
|
+
openDataRowUpdatePreview,
|
|
53
|
+
openEditorRowUpdatePreview,
|
|
52
54
|
refreshCurrentRoute,
|
|
53
55
|
refreshMediaTaggingPreview,
|
|
54
56
|
removeConnection,
|
|
@@ -66,6 +68,8 @@ import {
|
|
|
66
68
|
selectStructureEntry,
|
|
67
69
|
setTableDesignerSearchQuery,
|
|
68
70
|
setTableDesignerSqlPreviewVisibility,
|
|
71
|
+
setDataTableSearchQuery,
|
|
72
|
+
setStructureTableSearchQuery,
|
|
69
73
|
toggleStructureTablesPanel,
|
|
70
74
|
setDataPage,
|
|
71
75
|
setDataPageSize,
|
|
@@ -82,6 +86,7 @@ import {
|
|
|
82
86
|
submitCreateMediaTaggingTagTable,
|
|
83
87
|
submitCreateMediaTaggingMappingTable,
|
|
84
88
|
submitDeleteQueryHistoryConfirmation,
|
|
89
|
+
submitRowUpdatePreviewConfirmation,
|
|
85
90
|
setQueryHistoryPanelVisibility,
|
|
86
91
|
sortDataTableByColumn,
|
|
87
92
|
sortEditorResultsByColumn,
|
|
@@ -100,8 +105,6 @@ import {
|
|
|
100
105
|
submitCreateConnection,
|
|
101
106
|
createCurrentMediaTag,
|
|
102
107
|
submitDeleteRowConfirmation,
|
|
103
|
-
submitDataRowUpdate,
|
|
104
|
-
submitEditorRowUpdate,
|
|
105
108
|
submitEditConnection,
|
|
106
109
|
submitImportSql,
|
|
107
110
|
submitOpenConnection,
|
|
@@ -1775,6 +1778,16 @@ document.addEventListener('input', event => {
|
|
|
1775
1778
|
return;
|
|
1776
1779
|
}
|
|
1777
1780
|
|
|
1781
|
+
if (bindNode.dataset.bind === 'data-table-search') {
|
|
1782
|
+
setDataTableSearchQuery(bindNode.value);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (bindNode.dataset.bind === 'structure-table-search') {
|
|
1787
|
+
setStructureTableSearchQuery(bindNode.value);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1778
1791
|
if (bindNode.dataset.bind === 'table-designer-search') {
|
|
1779
1792
|
setTableDesignerSearchQuery(bindNode.value);
|
|
1780
1793
|
return;
|
|
@@ -2055,6 +2068,9 @@ document.addEventListener('submit', async event => {
|
|
|
2055
2068
|
case 'delete-query-history-confirm':
|
|
2056
2069
|
await submitDeleteQueryHistoryConfirmation();
|
|
2057
2070
|
return;
|
|
2071
|
+
case 'apply-row-update-preview':
|
|
2072
|
+
await submitRowUpdatePreviewConfirmation();
|
|
2073
|
+
return;
|
|
2058
2074
|
case 'create-media-tagging-tag-table':
|
|
2059
2075
|
await submitCreateMediaTaggingTagTable();
|
|
2060
2076
|
return;
|
|
@@ -2083,7 +2099,7 @@ document.addEventListener('submit', async event => {
|
|
|
2083
2099
|
}
|
|
2084
2100
|
}
|
|
2085
2101
|
|
|
2086
|
-
await
|
|
2102
|
+
await openDataRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values, rowIdentity);
|
|
2087
2103
|
return;
|
|
2088
2104
|
}
|
|
2089
2105
|
case 'save-editor-row': {
|
|
@@ -2097,7 +2113,7 @@ document.addEventListener('submit', async event => {
|
|
|
2097
2113
|
values[key.slice('field:'.length)] = String(value ?? '');
|
|
2098
2114
|
}
|
|
2099
2115
|
|
|
2100
|
-
await
|
|
2116
|
+
await openEditorRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values);
|
|
2101
2117
|
return;
|
|
2102
2118
|
}
|
|
2103
2119
|
case 'save-query-history-title': {
|
|
@@ -425,6 +425,98 @@ function renderDeleteRowConfirmForm(modal) {
|
|
|
425
425
|
].join("");
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
function renderRowUpdatePreviewForm(modal) {
|
|
429
|
+
const preview = modal.preview ?? {};
|
|
430
|
+
const changes = preview.changes ?? [];
|
|
431
|
+
const params = preview.params ?? [];
|
|
432
|
+
const warnings = preview.warnings ?? [];
|
|
433
|
+
const changesMarkup = changes.length
|
|
434
|
+
? [
|
|
435
|
+
'<div class="overflow-hidden border border-outline-variant/10">',
|
|
436
|
+
'<table class="w-full text-left text-sm">',
|
|
437
|
+
'<thead class="bg-surface-container-highest text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">',
|
|
438
|
+
'<tr><th class="px-3 py-2">Column</th><th class="px-3 py-2">Old</th><th class="px-3 py-2">New</th></tr>',
|
|
439
|
+
'</thead><tbody class="divide-y divide-outline-variant/10">',
|
|
440
|
+
changes
|
|
441
|
+
.map(
|
|
442
|
+
change => `
|
|
443
|
+
<tr>
|
|
444
|
+
<td class="px-3 py-2 font-mono text-xs text-primary-container">${escapeHtml(change.column)}</td>
|
|
445
|
+
<td class="px-3 py-2 text-on-surface-variant/70">${escapeHtml(change.oldValue)}</td>
|
|
446
|
+
<td class="px-3 py-2 text-on-surface">${escapeHtml(change.newValue)}</td>
|
|
447
|
+
</tr>
|
|
448
|
+
`,
|
|
449
|
+
)
|
|
450
|
+
.join(''),
|
|
451
|
+
'</tbody></table></div>',
|
|
452
|
+
].join('')
|
|
453
|
+
: '<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3 text-sm text-on-surface-variant/65">No changed values were detected.</div>';
|
|
454
|
+
const paramsMarkup = params.length
|
|
455
|
+
? `
|
|
456
|
+
<div class="space-y-2">
|
|
457
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">Parameters</div>
|
|
458
|
+
<div class="space-y-2">
|
|
459
|
+
${params
|
|
460
|
+
.map(
|
|
461
|
+
param => `
|
|
462
|
+
<div class="flex gap-3 border border-outline-variant/10 bg-surface-container-low px-3 py-2 text-xs">
|
|
463
|
+
<span class="font-mono text-primary-container">$${escapeHtml(param.index)}</span>
|
|
464
|
+
<span class="min-w-0 break-words text-on-surface">${escapeHtml(param.value)}</span>
|
|
465
|
+
</div>
|
|
466
|
+
`,
|
|
467
|
+
)
|
|
468
|
+
.join('')}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
`
|
|
472
|
+
: '';
|
|
473
|
+
const warningsMarkup = warnings.length
|
|
474
|
+
? `
|
|
475
|
+
<div class="space-y-2">
|
|
476
|
+
${warnings
|
|
477
|
+
.map(
|
|
478
|
+
warning => `
|
|
479
|
+
<div class="border border-primary-container/20 bg-primary-container/10 px-4 py-3 text-sm text-on-surface">
|
|
480
|
+
${escapeHtml(warning)}
|
|
481
|
+
</div>
|
|
482
|
+
`,
|
|
483
|
+
)
|
|
484
|
+
.join('')}
|
|
485
|
+
</div>
|
|
486
|
+
`
|
|
487
|
+
: '';
|
|
488
|
+
|
|
489
|
+
return `
|
|
490
|
+
<form class="space-y-5" data-form="apply-row-update-preview">
|
|
491
|
+
<div class="space-y-2">
|
|
492
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
493
|
+
${escapeHtml(modal.tableName ?? 'Table Row')}
|
|
494
|
+
</div>
|
|
495
|
+
<p class="text-sm leading-7 text-on-surface-variant/70">
|
|
496
|
+
Review the SQL and changed values before applying this row update.
|
|
497
|
+
</p>
|
|
498
|
+
</div>
|
|
499
|
+
${warningsMarkup}
|
|
500
|
+
<div class="space-y-2">
|
|
501
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">SQL Preview</div>
|
|
502
|
+
${renderSqlPreviewField(preview.sql ?? '', 'sql-highlight-shell--compact')}
|
|
503
|
+
</div>
|
|
504
|
+
<div class="space-y-2">
|
|
505
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">Changed Values</div>
|
|
506
|
+
${changesMarkup}
|
|
507
|
+
</div>
|
|
508
|
+
${paramsMarkup}
|
|
509
|
+
${renderError(modal.error)}
|
|
510
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
511
|
+
<button class="standard-button" data-action="close-modal" type="button">Cancel</button>
|
|
512
|
+
<button class="signature-button" type="submit">
|
|
513
|
+
${modal.submitting ? 'Applying...' : 'Apply Changes'}
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
</form>
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
519
|
+
|
|
428
520
|
function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
|
|
429
521
|
const options = allowEmpty ? [{ value: "", label: "None" }] : [];
|
|
430
522
|
|
|
@@ -869,6 +961,11 @@ export function renderModal(state) {
|
|
|
869
961
|
title: "Delete Row",
|
|
870
962
|
body: renderDeleteRowConfirmForm(modal),
|
|
871
963
|
},
|
|
964
|
+
"row-update-preview": {
|
|
965
|
+
eyebrow: "Mutation // Review row update",
|
|
966
|
+
title: "Review Update",
|
|
967
|
+
body: renderRowUpdatePreviewForm(modal),
|
|
968
|
+
},
|
|
872
969
|
"chart-editor": {
|
|
873
970
|
eyebrow: "Charts // Configure query-based ECharts panel",
|
|
874
971
|
title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
|
|
@@ -904,7 +1001,7 @@ export function renderModal(state) {
|
|
|
904
1001
|
|
|
905
1002
|
return `
|
|
906
1003
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
907
|
-
<div class="w-full ${modal.kind === "chart-editor" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
1004
|
+
<div class="w-full ${modal.kind === "chart-editor" || modal.kind === "row-update-preview" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
908
1005
|
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
909
1006
|
<div>
|
|
910
1007
|
<div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
|
|
@@ -38,7 +38,6 @@ function renderEditorSurface({ query }) {
|
|
|
38
38
|
|
|
39
39
|
export function renderQueryEditor({
|
|
40
40
|
query,
|
|
41
|
-
title,
|
|
42
41
|
executing = false,
|
|
43
42
|
exporting = false,
|
|
44
43
|
historyLoading = false,
|
|
@@ -48,13 +47,7 @@ export function renderQueryEditor({
|
|
|
48
47
|
}) {
|
|
49
48
|
const secondaryButtonClass = "standard-button";
|
|
50
49
|
const left = `
|
|
51
|
-
<div class="flex items-center gap-2
|
|
52
|
-
<span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
|
|
53
|
-
<span class="text-[10px] font-mono uppercase tracking-widest text-on-surface-variant">${escapeHtml(
|
|
54
|
-
title
|
|
55
|
-
)}</span>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
|
|
50
|
+
<div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40">
|
|
58
51
|
<span class="material-symbols-outlined text-xs">history</span>
|
|
59
52
|
${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
|
|
60
53
|
</div>
|
package/frontend/js/store.js
CHANGED
|
@@ -38,6 +38,7 @@ const UI_PREFERENCE_STORAGE_KEYS = {
|
|
|
38
38
|
sqlEditorHistoryVisible: 'sqlite_hub_sql_editor_history_visible',
|
|
39
39
|
sqlEditorEditorVisible: 'sqlite_hub_sql_editor_editor_visible',
|
|
40
40
|
sqlEditorActiveTab: 'sqlite_hub_sql_editor_active_tab',
|
|
41
|
+
sqlEditorQueryDraft: 'sqlite_hub_sql_editor_query_draft',
|
|
41
42
|
dataTablesVisible: 'sqlite_hub_data_tables_visible',
|
|
42
43
|
structureTablesVisible: 'sqlite_hub_structure_tables_visible',
|
|
43
44
|
chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
|
|
@@ -103,6 +104,23 @@ function storeBoolean(key, value) {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
function readStoredString(key, fallback = '') {
|
|
108
|
+
try {
|
|
109
|
+
const value = globalThis.localStorage?.getItem(key);
|
|
110
|
+
return value === null || value === undefined ? fallback : value;
|
|
111
|
+
} catch {
|
|
112
|
+
return fallback;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function storeString(key, value) {
|
|
117
|
+
try {
|
|
118
|
+
globalThis.localStorage?.setItem(key, String(value ?? ''));
|
|
119
|
+
} catch {
|
|
120
|
+
// Ignore unavailable browser storage; the in-memory value still applies.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
function readStoredEditorActiveTab(fallback = 'messages') {
|
|
107
125
|
try {
|
|
108
126
|
const value = globalThis.localStorage?.getItem(UI_PREFERENCE_STORAGE_KEYS.sqlEditorActiveTab);
|
|
@@ -193,6 +211,7 @@ const state = {
|
|
|
193
211
|
sortColumn: null,
|
|
194
212
|
sortDirection: null,
|
|
195
213
|
searchQuery: '',
|
|
214
|
+
tableSearchQuery: '',
|
|
196
215
|
searchColumn: '',
|
|
197
216
|
selectedRowIndex: null,
|
|
198
217
|
selectedRow: null,
|
|
@@ -202,7 +221,7 @@ const state = {
|
|
|
202
221
|
saveError: null,
|
|
203
222
|
},
|
|
204
223
|
editor: {
|
|
205
|
-
sqlText:
|
|
224
|
+
sqlText: readStoredString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft),
|
|
206
225
|
editorPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.sqlEditorEditorVisible, true),
|
|
207
226
|
history: [],
|
|
208
227
|
historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.sqlEditorHistoryVisible, true),
|
|
@@ -271,6 +290,7 @@ const state = {
|
|
|
271
290
|
selectedName: null,
|
|
272
291
|
detail: null,
|
|
273
292
|
tablesVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, true),
|
|
293
|
+
tableSearchQuery: '',
|
|
274
294
|
loading: false,
|
|
275
295
|
detailLoading: false,
|
|
276
296
|
error: null,
|
|
@@ -527,6 +547,10 @@ function resetDataBrowserSearch() {
|
|
|
527
547
|
state.dataBrowser.searchColumn = '';
|
|
528
548
|
}
|
|
529
549
|
|
|
550
|
+
function resetDataBrowserTableSearch() {
|
|
551
|
+
state.dataBrowser.tableSearchQuery = '';
|
|
552
|
+
}
|
|
553
|
+
|
|
530
554
|
function resetDataBrowserSort() {
|
|
531
555
|
state.dataBrowser.sortColumn = null;
|
|
532
556
|
state.dataBrowser.sortDirection = null;
|
|
@@ -707,7 +731,11 @@ function clearDataBrowserRowSelectionState() {
|
|
|
707
731
|
}
|
|
708
732
|
|
|
709
733
|
function resolveDataBrowserRowSelection(rowIndex, identity = null) {
|
|
710
|
-
const
|
|
734
|
+
const hasRowIndex =
|
|
735
|
+
rowIndex !== null &&
|
|
736
|
+
rowIndex !== undefined &&
|
|
737
|
+
(typeof rowIndex !== 'string' || rowIndex.trim() !== '');
|
|
738
|
+
const numericIndex = hasRowIndex ? Number(rowIndex) : NaN;
|
|
711
739
|
|
|
712
740
|
if (Number.isInteger(numericIndex) && numericIndex >= 0) {
|
|
713
741
|
const indexedRow = state.dataBrowser.table?.rows?.[numericIndex] ?? null;
|
|
@@ -951,6 +979,7 @@ function setMissingDatabaseState() {
|
|
|
951
979
|
state.dataBrowser.selectedTable = null;
|
|
952
980
|
state.dataBrowser.table = null;
|
|
953
981
|
state.dataBrowser.page = 1;
|
|
982
|
+
resetDataBrowserTableSearch();
|
|
954
983
|
resetDataBrowserSort();
|
|
955
984
|
resetDataBrowserSearch();
|
|
956
985
|
clearDataBrowserRowSelectionState();
|
|
@@ -964,6 +993,7 @@ function setMissingDatabaseState() {
|
|
|
964
993
|
state.structure.data = null;
|
|
965
994
|
state.structure.detail = null;
|
|
966
995
|
state.structure.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, true);
|
|
996
|
+
state.structure.tableSearchQuery = '';
|
|
967
997
|
state.structure.error = error;
|
|
968
998
|
|
|
969
999
|
state.tableDesigner.loading = false;
|
|
@@ -1919,6 +1949,7 @@ function invalidateDatabaseCaches() {
|
|
|
1919
1949
|
state.dataBrowser.selectedTable = null;
|
|
1920
1950
|
state.dataBrowser.table = null;
|
|
1921
1951
|
state.dataBrowser.page = 1;
|
|
1952
|
+
resetDataBrowserTableSearch();
|
|
1922
1953
|
resetDataBrowserSearch();
|
|
1923
1954
|
clearDataBrowserRowSelectionState();
|
|
1924
1955
|
state.dataBrowser.pendingOpenRow = null;
|
|
@@ -1939,6 +1970,7 @@ function invalidateDatabaseCaches() {
|
|
|
1939
1970
|
state.structure.data = null;
|
|
1940
1971
|
state.structure.detail = null;
|
|
1941
1972
|
state.structure.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.structureTablesVisible, true);
|
|
1973
|
+
state.structure.tableSearchQuery = '';
|
|
1942
1974
|
state.mediaTagging.loading = false;
|
|
1943
1975
|
state.mediaTagging.previewLoading = false;
|
|
1944
1976
|
state.mediaTagging.saving = false;
|
|
@@ -2356,6 +2388,92 @@ export function closeModal() {
|
|
|
2356
2388
|
closeModalInternal();
|
|
2357
2389
|
}
|
|
2358
2390
|
|
|
2391
|
+
export async function openDataRowUpdatePreview(rowIndex, values, identity = null) {
|
|
2392
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
2393
|
+
const selected = resolveDataBrowserRowSelection(rowIndex, identity);
|
|
2394
|
+
|
|
2395
|
+
if (!tableName || !selected.identity) {
|
|
2396
|
+
pushToast('The selected row could not be loaded.', 'alert');
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
state.dataBrowser.saving = true;
|
|
2401
|
+
state.dataBrowser.saveError = null;
|
|
2402
|
+
emitChange();
|
|
2403
|
+
|
|
2404
|
+
try {
|
|
2405
|
+
const response = await api.previewDataTableRowUpdate(tableName, {
|
|
2406
|
+
identity: selected.identity,
|
|
2407
|
+
values,
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
state.modal = {
|
|
2411
|
+
kind: 'row-update-preview',
|
|
2412
|
+
target: 'data',
|
|
2413
|
+
tableName,
|
|
2414
|
+
rowIndex: selected.rowIndex,
|
|
2415
|
+
identity: selected.identity,
|
|
2416
|
+
values,
|
|
2417
|
+
preview: response.data,
|
|
2418
|
+
error: null,
|
|
2419
|
+
submitting: false,
|
|
2420
|
+
};
|
|
2421
|
+
emitChange();
|
|
2422
|
+
return response.data;
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
state.dataBrowser.saveError = normalizeError(error);
|
|
2425
|
+
emitChange();
|
|
2426
|
+
return null;
|
|
2427
|
+
} finally {
|
|
2428
|
+
state.dataBrowser.saving = false;
|
|
2429
|
+
emitChange();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
export async function openEditorRowUpdatePreview(rowIndex, values) {
|
|
2434
|
+
const numericIndex = Number(rowIndex);
|
|
2435
|
+
const result = state.editor.result;
|
|
2436
|
+
const row = result?.rows?.[numericIndex];
|
|
2437
|
+
const tableName = result?.editing?.tableName ?? null;
|
|
2438
|
+
|
|
2439
|
+
if (!tableName || !row || !canEditQueryResult()) {
|
|
2440
|
+
pushToast('The selected query result row could not be loaded.', 'alert');
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
state.editor.saving = true;
|
|
2445
|
+
state.editor.saveError = null;
|
|
2446
|
+
emitChange();
|
|
2447
|
+
|
|
2448
|
+
try {
|
|
2449
|
+
const response = await api.previewDataTableRowUpdate(tableName, {
|
|
2450
|
+
identity: row.__identity,
|
|
2451
|
+
values,
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
state.modal = {
|
|
2455
|
+
kind: 'row-update-preview',
|
|
2456
|
+
target: 'editor',
|
|
2457
|
+
tableName,
|
|
2458
|
+
rowIndex: numericIndex,
|
|
2459
|
+
identity: row.__identity,
|
|
2460
|
+
values,
|
|
2461
|
+
preview: response.data,
|
|
2462
|
+
error: null,
|
|
2463
|
+
submitting: false,
|
|
2464
|
+
};
|
|
2465
|
+
emitChange();
|
|
2466
|
+
return response.data;
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
state.editor.saveError = normalizeError(error);
|
|
2469
|
+
emitChange();
|
|
2470
|
+
return null;
|
|
2471
|
+
} finally {
|
|
2472
|
+
state.editor.saving = false;
|
|
2473
|
+
emitChange();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2359
2477
|
export function toggleChartsSqlPanel() {
|
|
2360
2478
|
state.charts.sqlExpanded = !state.charts.sqlExpanded;
|
|
2361
2479
|
emitChange();
|
|
@@ -2623,6 +2741,7 @@ export function setCurrentQuery(query) {
|
|
|
2623
2741
|
const nextLineCount = Math.max(1, nextQuery.split('\n').length);
|
|
2624
2742
|
|
|
2625
2743
|
state.editor.sqlText = nextQuery;
|
|
2744
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft, nextQuery);
|
|
2626
2745
|
|
|
2627
2746
|
if (previousLineCount !== nextLineCount) {
|
|
2628
2747
|
emitChange();
|
|
@@ -2631,6 +2750,7 @@ export function setCurrentQuery(query) {
|
|
|
2631
2750
|
|
|
2632
2751
|
export function clearCurrentQuery() {
|
|
2633
2752
|
state.editor.sqlText = '';
|
|
2753
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft, '');
|
|
2634
2754
|
state.editor.result = null;
|
|
2635
2755
|
state.editor.lastExecutedSql = '';
|
|
2636
2756
|
resetEditorResultSort();
|
|
@@ -2696,6 +2816,8 @@ export async function executeCurrentQuery() {
|
|
|
2696
2816
|
return true;
|
|
2697
2817
|
} catch (error) {
|
|
2698
2818
|
state.editor.error = normalizeError(error);
|
|
2819
|
+
state.editor.activeTab = 'messages';
|
|
2820
|
+
storeEditorActiveTab('messages');
|
|
2699
2821
|
await refreshQueryHistoryState();
|
|
2700
2822
|
return false;
|
|
2701
2823
|
} finally {
|
|
@@ -2819,6 +2941,7 @@ export function openQueryHistoryInEditor(historyId, options = {}) {
|
|
|
2819
2941
|
setActiveQueryHistoryItem(historyId);
|
|
2820
2942
|
clearQueryHistoryDetailState();
|
|
2821
2943
|
state.editor.sqlText = options.append ? [state.editor.sqlText.trim(), rawSql].filter(Boolean).join('\n\n') : rawSql;
|
|
2944
|
+
storeString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft, state.editor.sqlText);
|
|
2822
2945
|
emitChange();
|
|
2823
2946
|
return true;
|
|
2824
2947
|
}
|
|
@@ -2926,6 +3049,16 @@ export function setTableDesignerSearchQuery(query) {
|
|
|
2926
3049
|
emitChange();
|
|
2927
3050
|
}
|
|
2928
3051
|
|
|
3052
|
+
export function setDataTableSearchQuery(query) {
|
|
3053
|
+
state.dataBrowser.tableSearchQuery = String(query ?? '');
|
|
3054
|
+
emitChange();
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
export function setStructureTableSearchQuery(query) {
|
|
3058
|
+
state.structure.tableSearchQuery = String(query ?? '');
|
|
3059
|
+
emitChange();
|
|
3060
|
+
}
|
|
3061
|
+
|
|
2929
3062
|
export function setTableDesignerSqlPreviewVisibility(visible) {
|
|
2930
3063
|
const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
|
|
2931
3064
|
|
|
@@ -3577,9 +3710,10 @@ export async function setDataPageSize(pageSize) {
|
|
|
3577
3710
|
}
|
|
3578
3711
|
}
|
|
3579
3712
|
|
|
3580
|
-
export async function submitDataRowUpdate(rowIndex, values, identity = null) {
|
|
3713
|
+
export async function submitDataRowUpdate(rowIndex, values, identity = null, options = {}) {
|
|
3581
3714
|
const tableName = state.dataBrowser.selectedTable;
|
|
3582
3715
|
const selected = resolveDataBrowserRowSelection(rowIndex, identity);
|
|
3716
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
3583
3717
|
|
|
3584
3718
|
if (!tableName || !selected.identity) {
|
|
3585
3719
|
pushToast('The selected row could not be loaded.', 'alert');
|
|
@@ -3601,8 +3735,12 @@ export async function submitDataRowUpdate(rowIndex, values, identity = null) {
|
|
|
3601
3735
|
clearDataBrowserRowSelectionState();
|
|
3602
3736
|
return response.data;
|
|
3603
3737
|
} catch (error) {
|
|
3604
|
-
|
|
3605
|
-
|
|
3738
|
+
if (reportErrorToModal) {
|
|
3739
|
+
withModalError(error);
|
|
3740
|
+
} else {
|
|
3741
|
+
state.dataBrowser.saveError = normalizeError(error);
|
|
3742
|
+
emitChange();
|
|
3743
|
+
}
|
|
3606
3744
|
return null;
|
|
3607
3745
|
} finally {
|
|
3608
3746
|
state.dataBrowser.saving = false;
|
|
@@ -3654,11 +3792,12 @@ export async function submitDataRowDelete(rowIndex, options = {}) {
|
|
|
3654
3792
|
}
|
|
3655
3793
|
}
|
|
3656
3794
|
|
|
3657
|
-
export async function submitEditorRowUpdate(rowIndex, values) {
|
|
3795
|
+
export async function submitEditorRowUpdate(rowIndex, values, options = {}) {
|
|
3658
3796
|
const numericIndex = Number(rowIndex);
|
|
3659
3797
|
const result = state.editor.result;
|
|
3660
3798
|
const row = result?.rows?.[numericIndex];
|
|
3661
3799
|
const tableName = result?.editing?.tableName ?? null;
|
|
3800
|
+
const reportErrorToModal = Boolean(options.reportErrorToModal);
|
|
3662
3801
|
|
|
3663
3802
|
if (!tableName || !row || !canEditQueryResult()) {
|
|
3664
3803
|
pushToast('The selected query result row could not be loaded.', 'alert');
|
|
@@ -3688,8 +3827,12 @@ export async function submitEditorRowUpdate(rowIndex, values) {
|
|
|
3688
3827
|
emitChange();
|
|
3689
3828
|
return response.data;
|
|
3690
3829
|
} catch (error) {
|
|
3691
|
-
|
|
3692
|
-
|
|
3830
|
+
if (reportErrorToModal) {
|
|
3831
|
+
withModalError(error);
|
|
3832
|
+
} else {
|
|
3833
|
+
state.editor.saveError = normalizeError(error);
|
|
3834
|
+
emitChange();
|
|
3835
|
+
}
|
|
3693
3836
|
return null;
|
|
3694
3837
|
} finally {
|
|
3695
3838
|
state.editor.saving = false;
|
|
@@ -3767,6 +3910,29 @@ export async function submitDeleteRowConfirmation() {
|
|
|
3767
3910
|
return result;
|
|
3768
3911
|
}
|
|
3769
3912
|
|
|
3913
|
+
export async function submitRowUpdatePreviewConfirmation() {
|
|
3914
|
+
const modal = state.modal;
|
|
3915
|
+
|
|
3916
|
+
if (modal?.kind !== 'row-update-preview') {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
startModalSubmission();
|
|
3921
|
+
|
|
3922
|
+
const result =
|
|
3923
|
+
modal.target === 'editor'
|
|
3924
|
+
? await submitEditorRowUpdate(modal.rowIndex, modal.values, { reportErrorToModal: true })
|
|
3925
|
+
: await submitDataRowUpdate(modal.rowIndex, modal.values, modal.identity, {
|
|
3926
|
+
reportErrorToModal: true,
|
|
3927
|
+
});
|
|
3928
|
+
|
|
3929
|
+
if (result) {
|
|
3930
|
+
closeModalInternal();
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
return result;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3770
3936
|
export async function saveCurrentQueryChartDraft() {
|
|
3771
3937
|
if (state.modal?.kind !== 'chart-editor' || !state.modal.draft) {
|
|
3772
3938
|
return null;
|
|
@@ -3988,12 +4154,12 @@ export function getQueryMessages(snapshot = state) {
|
|
|
3988
4154
|
|
|
3989
4155
|
if (snapshot.editor.error) {
|
|
3990
4156
|
return [
|
|
3991
|
-
...queryMessages,
|
|
3992
4157
|
{
|
|
3993
4158
|
tone: 'alert',
|
|
3994
4159
|
label: snapshot.editor.error.code,
|
|
3995
4160
|
value: snapshot.editor.error.message,
|
|
3996
4161
|
},
|
|
4162
|
+
...queryMessages,
|
|
3997
4163
|
];
|
|
3998
4164
|
}
|
|
3999
4165
|
|
|
@@ -4008,7 +4174,6 @@ export function getQueryMessages(snapshot = state) {
|
|
|
4008
4174
|
}
|
|
4009
4175
|
|
|
4010
4176
|
return [
|
|
4011
|
-
...queryMessages,
|
|
4012
4177
|
...snapshot.editor.result.statements.map(statement => ({
|
|
4013
4178
|
tone: statement.kind === 'resultSet' ? 'success' : inferStatusTone(statement.keyword),
|
|
4014
4179
|
label: `${statement.keyword} #${statement.index + 1}`,
|
|
@@ -4017,6 +4182,7 @@ export function getQueryMessages(snapshot = state) {
|
|
|
4017
4182
|
? `${statement.rowCount} row(s) returned.`
|
|
4018
4183
|
: `${statement.changes} row(s) affected.`,
|
|
4019
4184
|
})),
|
|
4185
|
+
...queryMessages,
|
|
4020
4186
|
];
|
|
4021
4187
|
}
|
|
4022
4188
|
|
|
@@ -16,8 +16,37 @@ function getSelectedRow(state) {
|
|
|
16
16
|
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function getFilteredTables(tables, searchQuery) {
|
|
20
|
+
const normalizedSearch = String(searchQuery ?? '')
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (!normalizedSearch) {
|
|
25
|
+
return tables;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderDataTableSearch(state) {
|
|
32
|
+
return `
|
|
33
|
+
<label class="table-designer-sidebar__search">
|
|
34
|
+
<span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
|
|
35
|
+
<input
|
|
36
|
+
class="table-designer-sidebar__search-input"
|
|
37
|
+
data-bind="data-table-search"
|
|
38
|
+
placeholder="Search tables..."
|
|
39
|
+
spellcheck="false"
|
|
40
|
+
type="search"
|
|
41
|
+
value="${escapeHtml(state.dataBrowser.tableSearchQuery ?? '')}"
|
|
42
|
+
/>
|
|
43
|
+
</label>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
function renderTableList(state) {
|
|
20
48
|
const tables = state.dataBrowser.tables ?? [];
|
|
49
|
+
const filteredTables = getFilteredTables(tables, state.dataBrowser.tableSearchQuery);
|
|
21
50
|
const activeName = state.dataBrowser.selectedTable;
|
|
22
51
|
|
|
23
52
|
if (state.dataBrowser.loading && !tables.length) {
|
|
@@ -39,10 +68,18 @@ function renderTableList(state) {
|
|
|
39
68
|
`;
|
|
40
69
|
}
|
|
41
70
|
|
|
71
|
+
if (!filteredTables.length) {
|
|
72
|
+
return `
|
|
73
|
+
<div class="px-6 py-6 text-sm text-on-surface-variant/55">
|
|
74
|
+
No tables match the current search.
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
42
79
|
return `
|
|
43
80
|
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
44
81
|
<div class="space-y-2">
|
|
45
|
-
${
|
|
82
|
+
${filteredTables
|
|
46
83
|
.map(
|
|
47
84
|
table => `
|
|
48
85
|
<button
|
|
@@ -556,6 +593,7 @@ export function renderDataView(state) {
|
|
|
556
593
|
total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
|
|
557
594
|
</div>
|
|
558
595
|
</div>
|
|
596
|
+
${renderDataTableSearch(state)}
|
|
559
597
|
${renderTableList(state)}
|
|
560
598
|
</aside>
|
|
561
599
|
`
|
|
@@ -4,7 +4,7 @@ import { renderQueryHistoryDetail } from '../components/queryHistoryDetail.js';
|
|
|
4
4
|
import { renderQueryHistoryPanel } from '../components/queryHistoryPanel.js';
|
|
5
5
|
import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
|
|
6
6
|
import { renderQueryResultsPane } from '../components/queryResults.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getQueryMessages, getQueryPerformance } from '../store.js';
|
|
8
8
|
import { escapeHtml, formatBytes, formatCellValue, formatExecutionTimeMs, formatNumber, isBlobPreview } from '../utils/format.js';
|
|
9
9
|
|
|
10
10
|
function renderMissingDatabase() {
|
|
@@ -244,7 +244,6 @@ function renderResultsSurface(state, isResultsRoute) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
247
|
-
const connection = getCurrentConnection(state);
|
|
248
247
|
const editorSectionClass = state.editor.editorPanelVisible ? 'min-h-[27.5%] max-h-[60%]' : 'flex-none';
|
|
249
248
|
const resultsSectionClass = 'flex-1';
|
|
250
249
|
|
|
@@ -261,7 +260,6 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
|
|
|
261
260
|
historyTotal: state.editor.historyTotal,
|
|
262
261
|
editorVisible: state.editor.editorPanelVisible,
|
|
263
262
|
historyVisible: state.editor.historyPanelVisible,
|
|
264
|
-
title: connection?.label ?? 'SQLite Query Workspace',
|
|
265
263
|
})}
|
|
266
264
|
</section>
|
|
267
265
|
<section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
|