sqlite-hub 0.4.0 → 0.6.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.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +15 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +114 -5
  12. package/frontend/js/app.js +368 -18
  13. package/frontend/js/components/bottomTabs.js +1 -1
  14. package/frontend/js/components/dataGrid.js +3 -3
  15. package/frontend/js/components/queryEditor.js +33 -55
  16. package/frontend/js/components/queryHistoryDetail.js +263 -0
  17. package/frontend/js/components/queryHistoryPanel.js +228 -0
  18. package/frontend/js/components/queryResults.js +32 -46
  19. package/frontend/js/components/rowEditorPanel.js +73 -14
  20. package/frontend/js/components/sidebar.js +1 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/router.js +10 -0
  25. package/frontend/js/store.js +841 -22
  26. package/frontend/js/utils/format.js +23 -0
  27. package/frontend/js/utils/tableDesigner.js +1192 -0
  28. package/frontend/js/views/data.js +273 -250
  29. package/frontend/js/views/editor.js +34 -10
  30. package/frontend/js/views/overview.js +15 -0
  31. package/frontend/js/views/tableDesigner.js +37 -0
  32. package/frontend/styles/base.css +87 -73
  33. package/frontend/styles/components.css +841 -188
  34. package/frontend/styles/views.css +40 -0
  35. package/package.json +1 -1
  36. package/server/routes/data.js +2 -0
  37. package/server/routes/export.js +4 -1
  38. package/server/routes/overview.js +12 -0
  39. package/server/routes/sql.js +163 -5
  40. package/server/routes/tableDesigner.js +60 -0
  41. package/server/server.js +5 -1
  42. package/server/services/sqlite/dataBrowserService.js +4 -16
  43. package/server/services/sqlite/exportService.js +4 -16
  44. package/server/services/sqlite/overviewService.js +34 -0
  45. package/server/services/sqlite/sqlExecutor.js +83 -63
  46. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  47. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  48. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  49. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  50. package/server/services/sqlite/tableDesignerService.js +181 -0
  51. package/server/services/sqlite/tableSort.js +63 -0
  52. package/server/services/storage/appStateStore.js +674 -1
  53. package/server/services/storage/queryHistoryUtils.js +169 -0
  54. package/frontend/assets/mockups/data_edit.png +0 -0
  55. package/frontend/assets/mockups/graph_visualize.png +0 -0
  56. package/frontend/assets/mockups/overview.png +0 -0
@@ -16,32 +16,47 @@ import {
16
16
  clearDataRowSelection,
17
17
  clearEditorRowSelection,
18
18
  clearEditorResults,
19
- clearSqlHistoryStateAndData,
19
+ clearQueryHistorySelection,
20
20
  closeModal,
21
+ deleteQueryHistoryStateItem,
21
22
  dismissToast,
22
23
  executeCurrentQuery,
23
24
  exportCurrentDataTableCsv,
24
25
  exportCurrentQueryCsv,
25
26
  getState,
26
27
  initializeApp,
27
- loadQueryFromHistory,
28
+ loadMoreQueryHistory,
28
29
  openModal,
30
+ openOverviewInFinder,
31
+ openQueryHistoryInEditor,
29
32
  openDeleteDataRowModal,
30
33
  openDeleteEditorRowModal,
31
34
  openEditConnectionModal,
32
35
  refreshCurrentRoute,
33
36
  removeConnection,
37
+ runQueryHistoryItem,
34
38
  selectDataRow,
35
39
  selectEditorRow,
36
40
  selectConnection,
41
+ selectQueryHistoryItem,
37
42
  selectStructureEntry,
43
+ setTableDesignerSearchQuery,
38
44
  setDataPage,
39
45
  setDataPageSize,
40
46
  setDataSearchColumn,
41
47
  setDataSearchQuery,
42
48
  setCurrentQuery,
43
49
  setEditorTab,
50
+ sortDataTableByColumn,
51
+ sortEditorResultsByColumn,
52
+ setQueryHistorySearchInput,
53
+ setQueryHistoryTab,
44
54
  setRoute,
55
+ saveQueryHistoryNotes,
56
+ saveQueryHistoryTitle,
57
+ saveCurrentTableDesignerDraft,
58
+ queueTableDesignerCsvImport,
59
+ showToast,
45
60
  submitCreateConnection,
46
61
  submitDeleteRowConfirmation,
47
62
  submitDataRowUpdate,
@@ -50,6 +65,11 @@ import {
50
65
  submitImportSql,
51
66
  submitOpenConnection,
52
67
  subscribe,
68
+ toggleQueryHistorySavedState,
69
+ updateCurrentTableDesignerColumnField,
70
+ updateCurrentTableDesignerField,
71
+ addCurrentTableDesignerColumn,
72
+ removeCurrentTableDesignerColumn,
53
73
  } from "./store.js";
54
74
  import { renderConnectionsView } from "./views/connections.js";
55
75
  import { renderDataView } from "./views/data.js";
@@ -58,6 +78,7 @@ import { renderLandingView } from "./views/landing.js";
58
78
  import { renderOverviewView } from "./views/overview.js";
59
79
  import { renderSettingsView } from "./views/settings.js";
60
80
  import { renderStructureView } from "./views/structure.js";
81
+ import { renderTableDesignerView } from "./views/tableDesigner.js";
61
82
  import { highlightSql } from "./utils/format.js";
62
83
 
63
84
  const appRoot = document.querySelector("#app");
@@ -74,6 +95,8 @@ const shellRefs = {
74
95
  modal: document.querySelector("#modal-root"),
75
96
  toast: document.querySelector("#toast-root"),
76
97
  };
98
+ let lastRenderedRoutePath = null;
99
+ let pendingNewTableDesignerAutofocus = false;
77
100
 
78
101
  function resetStructureGraphForDatabaseChange() {
79
102
  resetPersistedStructureGraphState();
@@ -134,6 +157,21 @@ function readFileAsBase64(file) {
134
157
  });
135
158
  }
136
159
 
160
+ function readFileAsText(file) {
161
+ return new Promise((resolve, reject) => {
162
+ const reader = new FileReader();
163
+
164
+ reader.onload = () => {
165
+ resolve(String(reader.result ?? ""));
166
+ };
167
+ reader.onerror = () => {
168
+ reject(reader.error ?? new Error("The selected CSV file could not be read."));
169
+ };
170
+
171
+ reader.readAsText(file);
172
+ });
173
+ }
174
+
137
175
  async function buildConnectionLogoUpload(file) {
138
176
  if (!(file instanceof File) || !file.size) {
139
177
  return null;
@@ -188,6 +226,8 @@ function resolveView(state) {
188
226
  return renderEditorView(state, { isResultsRoute: true });
189
227
  case "structure":
190
228
  return renderStructureView(state);
229
+ case "tableDesigner":
230
+ return renderTableDesignerView(state);
191
231
  case "settings":
192
232
  return renderSettingsView(state);
193
233
  default:
@@ -200,7 +240,11 @@ function captureFocusedInputState() {
200
240
 
201
241
  if (
202
242
  !activeElement ||
203
- !(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)
243
+ !(
244
+ activeElement instanceof HTMLInputElement ||
245
+ activeElement instanceof HTMLTextAreaElement ||
246
+ activeElement instanceof HTMLSelectElement
247
+ )
204
248
  ) {
205
249
  return null;
206
250
  }
@@ -212,6 +256,8 @@ function captureFocusedInputState() {
212
256
 
213
257
  return {
214
258
  bind,
259
+ field: activeElement.dataset.field ?? null,
260
+ columnId: activeElement.dataset.columnId ?? null,
215
261
  selectionStart: activeElement.selectionStart,
216
262
  selectionEnd: activeElement.selectionEnd,
217
263
  selectionDirection: activeElement.selectionDirection,
@@ -220,24 +266,61 @@ function captureFocusedInputState() {
220
266
  };
221
267
  }
222
268
 
269
+ function buildFocusedInputSelectors(snapshot) {
270
+ if (!snapshot?.bind) {
271
+ return [];
272
+ }
273
+
274
+ const selectors = [];
275
+ const bindSelector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
276
+
277
+ if (snapshot.columnId && snapshot.field) {
278
+ selectors.push(
279
+ `${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"][data-field="${CSS.escape(snapshot.field)}"]`
280
+ );
281
+ }
282
+
283
+ if (snapshot.field) {
284
+ selectors.push(`${bindSelector}[data-field="${CSS.escape(snapshot.field)}"]`);
285
+ }
286
+
287
+ if (snapshot.columnId) {
288
+ selectors.push(`${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"]`);
289
+ }
290
+
291
+ selectors.push(bindSelector);
292
+ return selectors;
293
+ }
294
+
223
295
  function restoreFocusedInputState(snapshot) {
224
296
  if (!snapshot) {
225
- return;
297
+ return false;
226
298
  }
227
299
 
228
- const selector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
229
- const nextElement = document.querySelector(selector);
300
+ const nextElement = buildFocusedInputSelectors(snapshot)
301
+ .map((selector) => document.querySelector(selector))
302
+ .find(
303
+ (candidate) =>
304
+ candidate instanceof HTMLInputElement ||
305
+ candidate instanceof HTMLTextAreaElement ||
306
+ candidate instanceof HTMLSelectElement
307
+ );
230
308
 
231
309
  if (
232
310
  !nextElement ||
233
- !(nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement)
311
+ !(
312
+ nextElement instanceof HTMLInputElement ||
313
+ nextElement instanceof HTMLTextAreaElement ||
314
+ nextElement instanceof HTMLSelectElement
315
+ )
234
316
  ) {
235
- return;
317
+ return false;
236
318
  }
237
319
 
238
320
  nextElement.focus({ preventScroll: true });
239
321
 
240
322
  if (
323
+ (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) &&
241
324
  typeof snapshot.selectionStart === "number" &&
242
325
  typeof snapshot.selectionEnd === "number"
243
326
  ) {
@@ -250,14 +333,97 @@ function restoreFocusedInputState(snapshot) {
250
333
 
251
334
  nextElement.scrollTop = snapshot.scrollTop;
252
335
  nextElement.scrollLeft = snapshot.scrollLeft;
336
+ return true;
337
+ }
338
+
339
+ function focusNewTableDesignerNameField() {
340
+ const input = document.querySelector(
341
+ '[data-bind="table-designer-field"][data-field="tableName"]'
342
+ );
343
+
344
+ if (!(input instanceof HTMLInputElement)) {
345
+ return false;
346
+ }
347
+
348
+ input.focus({ preventScroll: true });
349
+ input.setSelectionRange(input.value.length, input.value.length);
350
+ return true;
351
+ }
352
+
353
+ function focusTableDesignerColumnNameField(columnId) {
354
+ if (!columnId) {
355
+ return false;
356
+ }
357
+
358
+ const input = document.querySelector(
359
+ `[data-bind="table-designer-column-field"][data-column-id="${CSS.escape(columnId)}"][data-field="name"]`
360
+ );
361
+
362
+ if (!(input instanceof HTMLInputElement)) {
363
+ return false;
364
+ }
365
+
366
+ input.focus({ preventScroll: true });
367
+ input.setSelectionRange(input.value.length, input.value.length);
368
+ return true;
369
+ }
370
+
371
+ async function handleTableDesignerCsvImport(fileInput) {
372
+ if (!(fileInput instanceof HTMLInputElement)) {
373
+ return;
374
+ }
375
+
376
+ const file = fileInput.files?.[0];
377
+
378
+ if (!(file instanceof File)) {
379
+ return;
380
+ }
381
+
382
+ if (!file.size) {
383
+ showToast("The selected CSV file is empty.", "alert");
384
+ fileInput.value = "";
385
+ return;
386
+ }
387
+
388
+ try {
389
+ const csvText = await readFileAsText(file);
390
+ const imported = queueTableDesignerCsvImport(file.name, csvText);
391
+
392
+ if (!imported) {
393
+ return;
394
+ }
395
+
396
+ router.navigate("/table-designer/new");
397
+ showToast(
398
+ `Imported ${imported.columnCount} columns and ${imported.importedRowCount} row${
399
+ imported.importedRowCount === 1 ? "" : "s"
400
+ } from ${file.name}.`,
401
+ "success"
402
+ );
403
+ } catch (error) {
404
+ showToast(error?.message || "CSV import failed.", "alert");
405
+ } finally {
406
+ fileInput.value = "";
407
+ }
253
408
  }
254
409
 
255
410
  function renderApp(state) {
256
411
  const focusedInput = captureFocusedInputState();
412
+ const previousRoutePath = lastRenderedRoutePath;
257
413
  const { main, panel } = resolveView(state);
258
- const isLockedRoute = ["editor", "editorResults", "data", "structure"].includes(
414
+ const isLockedRoute = ["editor", "editorResults", "data", "structure", "tableDesigner"].includes(
259
415
  state.route.name
260
416
  );
417
+ const isEnteringNewTableDesignerRoute =
418
+ state.route.name === "tableDesigner" &&
419
+ state.route.params?.isNew &&
420
+ previousRoutePath !== state.route.path;
421
+
422
+ if (isEnteringNewTableDesignerRoute) {
423
+ pendingNewTableDesignerAutofocus = true;
424
+ } else if (state.route.name !== "tableDesigner" || !state.route.params?.isNew) {
425
+ pendingNewTableDesignerAutofocus = false;
426
+ }
261
427
 
262
428
  teardownStructureGraph();
263
429
  shellRefs.topNav.innerHTML = renderTopNav(state);
@@ -269,7 +435,21 @@ function renderApp(state) {
269
435
  shellRefs.modal.innerHTML = renderModal(state);
270
436
  shellRefs.toast.innerHTML = renderToasts(state.toasts);
271
437
  shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
272
- restoreFocusedInputState(focusedInput);
438
+
439
+ if (
440
+ pendingNewTableDesignerAutofocus &&
441
+ state.route.name === "tableDesigner" &&
442
+ state.route.params?.isNew &&
443
+ state.tableDesigner.draft?.mode === "create"
444
+ ) {
445
+ if (focusNewTableDesignerNameField()) {
446
+ pendingNewTableDesignerAutofocus = false;
447
+ }
448
+ } else {
449
+ restoreFocusedInputState(focusedInput);
450
+ }
451
+
452
+ lastRenderedRoutePath = state.route.path;
273
453
 
274
454
  if (state.route.name === "structure") {
275
455
  mountStructureGraph(state).catch((error) => {
@@ -338,6 +518,9 @@ async function handleAction(actionNode) {
338
518
  case "create-backup":
339
519
  await createActiveConnectionBackup();
340
520
  return;
521
+ case "open-overview-in-finder":
522
+ await openOverviewInFinder();
523
+ return;
341
524
  case "execute-query": {
342
525
  await executeEditorQueryAndNavigate();
343
526
  return;
@@ -350,11 +533,57 @@ async function handleAction(actionNode) {
350
533
  return;
351
534
  case "clear-query":
352
535
  clearCurrentQuery();
536
+ if (getState().route.name === "editorResults") {
537
+ router.navigate("/editor");
538
+ }
353
539
  return;
354
540
  case "clear-results":
355
541
  clearEditorResults();
356
542
  router.navigate("/editor");
357
543
  return;
544
+ case "select-query-history-item":
545
+ if (actionNode.dataset.historyId) {
546
+ await selectQueryHistoryItem(actionNode.dataset.historyId);
547
+ }
548
+ return;
549
+ case "clear-query-history-selection":
550
+ clearQueryHistorySelection();
551
+ return;
552
+ case "set-query-history-tab":
553
+ if (actionNode.dataset.tab) {
554
+ await setQueryHistoryTab(actionNode.dataset.tab);
555
+ }
556
+ return;
557
+ case "load-more-query-history":
558
+ await loadMoreQueryHistory();
559
+ return;
560
+ case "open-query-history":
561
+ if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
562
+ router.navigate("/editor");
563
+ }
564
+ return;
565
+ case "run-query-history":
566
+ if (actionNode.dataset.historyId) {
567
+ const success = await runQueryHistoryItem(actionNode.dataset.historyId);
568
+ router.navigate(success ? "/editor/results" : "/editor");
569
+ }
570
+ return;
571
+ case "toggle-query-history-saved":
572
+ if (actionNode.dataset.historyId) {
573
+ await toggleQueryHistorySavedState(
574
+ actionNode.dataset.historyId,
575
+ actionNode.dataset.nextValue === "true"
576
+ );
577
+ }
578
+ return;
579
+ case "delete-query-history":
580
+ if (
581
+ actionNode.dataset.historyId &&
582
+ window.confirm("Delete this query history item and all recorded runs?")
583
+ ) {
584
+ await deleteQueryHistoryStateItem(actionNode.dataset.historyId);
585
+ }
586
+ return;
358
587
  case "set-editor-tab": {
359
588
  const tab = actionNode.dataset.tab;
360
589
  if (!tab) {
@@ -364,9 +593,6 @@ async function handleAction(actionNode) {
364
593
  router.navigate(tab === "results" ? "/editor/results" : "/editor");
365
594
  return;
366
595
  }
367
- case "clear-sql-history":
368
- await clearSqlHistoryStateAndData();
369
- return;
370
596
  case "export-query-csv":
371
597
  await exportCurrentQueryCsv();
372
598
  return;
@@ -378,6 +604,53 @@ async function handleAction(actionNode) {
378
604
  await selectStructureEntry(actionNode.dataset.entryName);
379
605
  }
380
606
  return;
607
+ case "add-table-designer-column":
608
+ {
609
+ const columnId = addCurrentTableDesignerColumn();
610
+ if (columnId) {
611
+ window.requestAnimationFrame(() => {
612
+ focusTableDesignerColumnNameField(columnId);
613
+ });
614
+ }
615
+ }
616
+ return;
617
+ case "remove-table-designer-column":
618
+ if (actionNode.dataset.columnId) {
619
+ removeCurrentTableDesignerColumn(actionNode.dataset.columnId);
620
+ }
621
+ return;
622
+ case "save-table-designer": {
623
+ const savedTableName = await saveCurrentTableDesignerDraft();
624
+ if (savedTableName) {
625
+ router.navigate(`/table-designer/${encodeURIComponent(savedTableName)}`);
626
+ }
627
+ return;
628
+ }
629
+ case "copy-table-designer-sql": {
630
+ const sqlPreview = getState().tableDesigner.draft?.sqlPreview ?? "";
631
+ if (!sqlPreview.trim()) {
632
+ return;
633
+ }
634
+
635
+ try {
636
+ await navigator.clipboard.writeText(sqlPreview);
637
+ showToast("SQL preview copied.", "success");
638
+ } catch (error) {
639
+ showToast("Clipboard access failed.", "alert");
640
+ }
641
+ return;
642
+ }
643
+ case "import-table-designer-csv": {
644
+ const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
645
+
646
+ if (!(fileInput instanceof HTMLInputElement)) {
647
+ return;
648
+ }
649
+
650
+ fileInput.value = "";
651
+ fileInput.click();
652
+ return;
653
+ }
381
654
  case "select-data-row":
382
655
  if (actionNode.dataset.rowIndex) {
383
656
  selectDataRow(actionNode.dataset.rowIndex);
@@ -399,6 +672,16 @@ async function handleAction(actionNode) {
399
672
  await setDataPage(actionNode.dataset.page);
400
673
  }
401
674
  return;
675
+ case "sort-data-column":
676
+ if (actionNode.dataset.columnName) {
677
+ await sortDataTableByColumn(actionNode.dataset.columnName);
678
+ }
679
+ return;
680
+ case "sort-editor-results-column":
681
+ if (actionNode.dataset.columnName) {
682
+ sortEditorResultsByColumn(actionNode.dataset.columnName);
683
+ }
684
+ return;
402
685
  case "set-data-page-size":
403
686
  if (actionNode.dataset.pageSize) {
404
687
  await setDataPageSize(actionNode.dataset.pageSize);
@@ -483,6 +766,34 @@ document.addEventListener("input", (event) => {
483
766
 
484
767
  if (bindNode.dataset.bind === "data-search-query") {
485
768
  setDataSearchQuery(bindNode.value);
769
+ return;
770
+ }
771
+
772
+ if (bindNode.dataset.bind === "table-designer-search") {
773
+ setTableDesignerSearchQuery(bindNode.value);
774
+ return;
775
+ }
776
+
777
+ if (bindNode.dataset.bind === "table-designer-field") {
778
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
779
+ return;
780
+ }
781
+
782
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
783
+ return;
784
+ }
785
+
786
+ if (bindNode.dataset.bind === "table-designer-column-field") {
787
+ updateCurrentTableDesignerColumnField(
788
+ bindNode.dataset.columnId,
789
+ bindNode.dataset.field,
790
+ bindNode.value
791
+ );
792
+ return;
793
+ }
794
+
795
+ if (bindNode.dataset.bind === "query-history-search") {
796
+ setQueryHistorySearchInput(bindNode.value);
486
797
  }
487
798
  });
488
799
 
@@ -507,14 +818,41 @@ document.addEventListener("change", (event) => {
507
818
  return;
508
819
  }
509
820
 
510
- if (bindNode.dataset.bind === "history-entry" && bindNode.value) {
511
- loadQueryFromHistory(bindNode.value);
512
- bindNode.value = "";
821
+ if (bindNode.dataset.bind === "data-search-column") {
822
+ setDataSearchColumn(bindNode.value);
513
823
  return;
514
824
  }
515
825
 
516
- if (bindNode.dataset.bind === "data-search-column") {
517
- setDataSearchColumn(bindNode.value);
826
+ if (bindNode.dataset.bind === "table-designer-import-file") {
827
+ void handleTableDesignerCsvImport(bindNode);
828
+ return;
829
+ }
830
+
831
+ if (bindNode.dataset.bind === "table-designer-field") {
832
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
833
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
834
+ return;
835
+ }
836
+
837
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
838
+ return;
839
+ }
840
+
841
+ if (bindNode.dataset.bind === "table-designer-column-field") {
842
+ updateCurrentTableDesignerColumnField(
843
+ bindNode.dataset.columnId,
844
+ bindNode.dataset.field,
845
+ bindNode.value
846
+ );
847
+ return;
848
+ }
849
+
850
+ if (bindNode.dataset.bind === "table-designer-column-flag") {
851
+ updateCurrentTableDesignerColumnField(
852
+ bindNode.dataset.columnId,
853
+ bindNode.dataset.field,
854
+ bindNode.checked
855
+ );
518
856
  }
519
857
  });
520
858
 
@@ -634,6 +972,18 @@ document.addEventListener("submit", async (event) => {
634
972
  );
635
973
  return;
636
974
  }
975
+ case "save-query-history-title":
976
+ await saveQueryHistoryTitle(
977
+ String(formData.get("historyId") ?? ""),
978
+ String(formData.get("title") ?? "")
979
+ );
980
+ return;
981
+ case "save-query-history-notes":
982
+ await saveQueryHistoryNotes(
983
+ String(formData.get("historyId") ?? ""),
984
+ String(formData.get("notes") ?? "")
985
+ );
986
+ return;
637
987
  default:
638
988
  }
639
989
  });
@@ -3,8 +3,8 @@ import { escapeHtml } from "../utils/format.js";
3
3
  export function renderBottomTabs(activeTab, counts = {}) {
4
4
  const tabs = [
5
5
  { key: "results", label: "results", meta: counts.resultRows ?? 0 },
6
- { key: "messages", label: "messages", meta: counts.messages ?? 0 },
7
6
  { key: "performance", label: "performance", meta: counts.statementCount ?? 0 },
7
+ { key: "messages", label: "messages", meta: counts.messages ?? 0 },
8
8
  ];
9
9
 
10
10
  return `
@@ -14,9 +14,9 @@ export function renderDataGrid({
14
14
  <tr class="${headerRowClass}">
15
15
  ${columns
16
16
  .map(
17
- (column) => `
18
- <th class="${column.headerClassName ?? ""}">
19
- ${column.label}
17
+ (column) => `
18
+ <th class="${column.headerClassName ?? ""}" ${column.headerAttrs ?? ""}>
19
+ ${column.renderHeader ? column.renderHeader() : column.label}
20
20
  </th>
21
21
  `
22
22
  )