sqlite-hub 0.5.0 → 0.7.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 (50) hide show
  1. package/README.md +109 -2
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +15 -0
  4. package/frontend/assets/mockups/connections.png +0 -0
  5. package/frontend/assets/mockups/data.png +0 -0
  6. package/frontend/assets/mockups/data_row_editor.png +0 -0
  7. package/frontend/assets/mockups/home.png +0 -0
  8. package/frontend/assets/mockups/sql_editor.png +0 -0
  9. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  10. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  11. package/frontend/assets/mockups/structure.png +0 -0
  12. package/frontend/assets/mockups/structure_inspector.png +0 -0
  13. package/frontend/index.html +1 -0
  14. package/frontend/js/api.js +49 -0
  15. package/frontend/js/app.js +377 -9
  16. package/frontend/js/components/modal.js +314 -1
  17. package/frontend/js/components/queryChartRenderer.js +125 -0
  18. package/frontend/js/components/queryEditor.js +37 -8
  19. package/frontend/js/components/queryHistoryPanel.js +16 -5
  20. package/frontend/js/components/sidebar.js +2 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +129 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/lib/queryChartOptions.js +283 -0
  25. package/frontend/js/lib/queryCharts.js +560 -0
  26. package/frontend/js/router.js +18 -0
  27. package/frontend/js/store.js +914 -1
  28. package/frontend/js/utils/tableDesigner.js +1192 -0
  29. package/frontend/js/views/charts.js +471 -0
  30. package/frontend/js/views/data.js +281 -277
  31. package/frontend/js/views/editor.js +19 -13
  32. package/frontend/js/views/structure.js +81 -39
  33. package/frontend/js/views/tableDesigner.js +37 -0
  34. package/frontend/styles/base.css +87 -73
  35. package/frontend/styles/components.css +790 -251
  36. package/frontend/styles/views.css +316 -46
  37. package/package.json +2 -1
  38. package/server/routes/charts.js +153 -0
  39. package/server/routes/tableDesigner.js +60 -0
  40. package/server/server.js +10 -0
  41. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  42. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  43. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  44. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  45. package/server/services/sqlite/tableDesignerService.js +181 -0
  46. package/server/services/storage/appStateStore.js +400 -1
  47. package/server/services/storage/queryHistoryChartUtils.js +145 -0
  48. package/frontend/assets/mockups/data_edit.png +0 -0
  49. package/frontend/assets/mockups/graph_visualize.png +0 -0
  50. package/frontend/assets/mockups/overview.png +0 -0
@@ -4,9 +4,14 @@ import { renderSidebar } from "./components/sidebar.js";
4
4
  import { renderStatusBar } from "./components/statusBar.js";
5
5
  import {
6
6
  mountStructureGraph,
7
- resetPersistedStructureGraphState,
8
7
  teardownStructureGraph,
8
+ resetPersistedStructureGraphState,
9
9
  } from "./components/structureGraph.js";
10
+ import {
11
+ exportQueryChartAsPng,
12
+ mountQueryChartRenderer,
13
+ teardownQueryChartRenderer,
14
+ } from "./components/queryChartRenderer.js";
10
15
  import { renderToasts } from "./components/toast.js";
11
16
  import { renderTopNav } from "./components/topNav.js";
12
17
  import { createRouter } from "./router.js";
@@ -31,21 +36,31 @@ import {
31
36
  openQueryHistoryInEditor,
32
37
  openDeleteDataRowModal,
33
38
  openDeleteEditorRowModal,
39
+ openDeleteQueryChartModal,
34
40
  openEditConnectionModal,
41
+ openCreateQueryChartModal,
42
+ openEditQueryChartModal,
35
43
  refreshCurrentRoute,
36
44
  removeConnection,
37
45
  runQueryHistoryItem,
46
+ saveCurrentQueryChartDraft,
38
47
  selectDataRow,
39
48
  selectEditorRow,
40
49
  selectConnection,
41
50
  selectQueryHistoryItem,
42
51
  selectStructureEntry,
52
+ setTableDesignerSearchQuery,
43
53
  setDataPage,
44
54
  setDataPageSize,
45
55
  setDataSearchColumn,
46
56
  setDataSearchQuery,
57
+ toggleDataTablesPanel,
47
58
  setCurrentQuery,
59
+ setChartsHeightPreset,
60
+ setEditorPanelVisibility,
48
61
  setEditorTab,
62
+ submitDeleteChartConfirmation,
63
+ setQueryHistoryPanelVisibility,
49
64
  sortDataTableByColumn,
50
65
  sortEditorResultsByColumn,
51
66
  setQueryHistorySearchInput,
@@ -53,6 +68,11 @@ import {
53
68
  setRoute,
54
69
  saveQueryHistoryNotes,
55
70
  saveQueryHistoryTitle,
71
+ saveCurrentTableDesignerDraft,
72
+ toggleChartsResultsPanel,
73
+ toggleChartsSqlPanel,
74
+ queueTableDesignerCsvImport,
75
+ showToast,
56
76
  submitCreateConnection,
57
77
  submitDeleteRowConfirmation,
58
78
  submitDataRowUpdate,
@@ -62,7 +82,14 @@ import {
62
82
  submitOpenConnection,
63
83
  subscribe,
64
84
  toggleQueryHistorySavedState,
85
+ updateCurrentQueryChartDraftConfigField,
86
+ updateCurrentQueryChartDraftField,
87
+ updateCurrentTableDesignerColumnField,
88
+ updateCurrentTableDesignerField,
89
+ addCurrentTableDesignerColumn,
90
+ removeCurrentTableDesignerColumn,
65
91
  } from "./store.js";
92
+ import { renderChartsView } from "./views/charts.js";
66
93
  import { renderConnectionsView } from "./views/connections.js";
67
94
  import { renderDataView } from "./views/data.js";
68
95
  import { renderEditorView } from "./views/editor.js";
@@ -70,6 +97,7 @@ import { renderLandingView } from "./views/landing.js";
70
97
  import { renderOverviewView } from "./views/overview.js";
71
98
  import { renderSettingsView } from "./views/settings.js";
72
99
  import { renderStructureView } from "./views/structure.js";
100
+ import { renderTableDesignerView } from "./views/tableDesigner.js";
73
101
  import { highlightSql } from "./utils/format.js";
74
102
 
75
103
  const appRoot = document.querySelector("#app");
@@ -86,6 +114,8 @@ const shellRefs = {
86
114
  modal: document.querySelector("#modal-root"),
87
115
  toast: document.querySelector("#toast-root"),
88
116
  };
117
+ let lastRenderedRoutePath = null;
118
+ let pendingNewTableDesignerAutofocus = false;
89
119
 
90
120
  function resetStructureGraphForDatabaseChange() {
91
121
  resetPersistedStructureGraphState();
@@ -146,6 +176,21 @@ function readFileAsBase64(file) {
146
176
  });
147
177
  }
148
178
 
179
+ function readFileAsText(file) {
180
+ return new Promise((resolve, reject) => {
181
+ const reader = new FileReader();
182
+
183
+ reader.onload = () => {
184
+ resolve(String(reader.result ?? ""));
185
+ };
186
+ reader.onerror = () => {
187
+ reject(reader.error ?? new Error("The selected CSV file could not be read."));
188
+ };
189
+
190
+ reader.readAsText(file);
191
+ });
192
+ }
193
+
149
194
  async function buildConnectionLogoUpload(file) {
150
195
  if (!(file instanceof File) || !file.size) {
151
196
  return null;
@@ -192,6 +237,8 @@ function resolveView(state) {
192
237
  return renderConnectionsView(state);
193
238
  case "overview":
194
239
  return renderOverviewView(state);
240
+ case "charts":
241
+ return renderChartsView(state);
195
242
  case "data":
196
243
  return renderDataView(state);
197
244
  case "editor":
@@ -200,6 +247,8 @@ function resolveView(state) {
200
247
  return renderEditorView(state, { isResultsRoute: true });
201
248
  case "structure":
202
249
  return renderStructureView(state);
250
+ case "tableDesigner":
251
+ return renderTableDesignerView(state);
203
252
  case "settings":
204
253
  return renderSettingsView(state);
205
254
  default:
@@ -212,7 +261,11 @@ function captureFocusedInputState() {
212
261
 
213
262
  if (
214
263
  !activeElement ||
215
- !(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)
264
+ !(
265
+ activeElement instanceof HTMLInputElement ||
266
+ activeElement instanceof HTMLTextAreaElement ||
267
+ activeElement instanceof HTMLSelectElement
268
+ )
216
269
  ) {
217
270
  return null;
218
271
  }
@@ -224,6 +277,8 @@ function captureFocusedInputState() {
224
277
 
225
278
  return {
226
279
  bind,
280
+ field: activeElement.dataset.field ?? null,
281
+ columnId: activeElement.dataset.columnId ?? null,
227
282
  selectionStart: activeElement.selectionStart,
228
283
  selectionEnd: activeElement.selectionEnd,
229
284
  selectionDirection: activeElement.selectionDirection,
@@ -232,24 +287,61 @@ function captureFocusedInputState() {
232
287
  };
233
288
  }
234
289
 
290
+ function buildFocusedInputSelectors(snapshot) {
291
+ if (!snapshot?.bind) {
292
+ return [];
293
+ }
294
+
295
+ const selectors = [];
296
+ const bindSelector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
297
+
298
+ if (snapshot.columnId && snapshot.field) {
299
+ selectors.push(
300
+ `${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"][data-field="${CSS.escape(snapshot.field)}"]`
301
+ );
302
+ }
303
+
304
+ if (snapshot.field) {
305
+ selectors.push(`${bindSelector}[data-field="${CSS.escape(snapshot.field)}"]`);
306
+ }
307
+
308
+ if (snapshot.columnId) {
309
+ selectors.push(`${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"]`);
310
+ }
311
+
312
+ selectors.push(bindSelector);
313
+ return selectors;
314
+ }
315
+
235
316
  function restoreFocusedInputState(snapshot) {
236
317
  if (!snapshot) {
237
- return;
318
+ return false;
238
319
  }
239
320
 
240
- const selector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
241
- const nextElement = document.querySelector(selector);
321
+ const nextElement = buildFocusedInputSelectors(snapshot)
322
+ .map((selector) => document.querySelector(selector))
323
+ .find(
324
+ (candidate) =>
325
+ candidate instanceof HTMLInputElement ||
326
+ candidate instanceof HTMLTextAreaElement ||
327
+ candidate instanceof HTMLSelectElement
328
+ );
242
329
 
243
330
  if (
244
331
  !nextElement ||
245
- !(nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement)
332
+ !(
333
+ nextElement instanceof HTMLInputElement ||
334
+ nextElement instanceof HTMLTextAreaElement ||
335
+ nextElement instanceof HTMLSelectElement
336
+ )
246
337
  ) {
247
- return;
338
+ return false;
248
339
  }
249
340
 
250
341
  nextElement.focus({ preventScroll: true });
251
342
 
252
343
  if (
344
+ (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) &&
253
345
  typeof snapshot.selectionStart === "number" &&
254
346
  typeof snapshot.selectionEnd === "number"
255
347
  ) {
@@ -262,16 +354,100 @@ function restoreFocusedInputState(snapshot) {
262
354
 
263
355
  nextElement.scrollTop = snapshot.scrollTop;
264
356
  nextElement.scrollLeft = snapshot.scrollLeft;
357
+ return true;
358
+ }
359
+
360
+ function focusNewTableDesignerNameField() {
361
+ const input = document.querySelector(
362
+ '[data-bind="table-designer-field"][data-field="tableName"]'
363
+ );
364
+
365
+ if (!(input instanceof HTMLInputElement)) {
366
+ return false;
367
+ }
368
+
369
+ input.focus({ preventScroll: true });
370
+ input.setSelectionRange(input.value.length, input.value.length);
371
+ return true;
372
+ }
373
+
374
+ function focusTableDesignerColumnNameField(columnId) {
375
+ if (!columnId) {
376
+ return false;
377
+ }
378
+
379
+ const input = document.querySelector(
380
+ `[data-bind="table-designer-column-field"][data-column-id="${CSS.escape(columnId)}"][data-field="name"]`
381
+ );
382
+
383
+ if (!(input instanceof HTMLInputElement)) {
384
+ return false;
385
+ }
386
+
387
+ input.focus({ preventScroll: true });
388
+ input.setSelectionRange(input.value.length, input.value.length);
389
+ return true;
390
+ }
391
+
392
+ async function handleTableDesignerCsvImport(fileInput) {
393
+ if (!(fileInput instanceof HTMLInputElement)) {
394
+ return;
395
+ }
396
+
397
+ const file = fileInput.files?.[0];
398
+
399
+ if (!(file instanceof File)) {
400
+ return;
401
+ }
402
+
403
+ if (!file.size) {
404
+ showToast("The selected CSV file is empty.", "alert");
405
+ fileInput.value = "";
406
+ return;
407
+ }
408
+
409
+ try {
410
+ const csvText = await readFileAsText(file);
411
+ const imported = queueTableDesignerCsvImport(file.name, csvText);
412
+
413
+ if (!imported) {
414
+ return;
415
+ }
416
+
417
+ router.navigate("/table-designer/new");
418
+ showToast(
419
+ `Imported ${imported.columnCount} columns and ${imported.importedRowCount} row${
420
+ imported.importedRowCount === 1 ? "" : "s"
421
+ } from ${file.name}.`,
422
+ "success"
423
+ );
424
+ } catch (error) {
425
+ showToast(error?.message || "CSV import failed.", "alert");
426
+ } finally {
427
+ fileInput.value = "";
428
+ }
265
429
  }
266
430
 
267
431
  function renderApp(state) {
268
432
  const focusedInput = captureFocusedInputState();
433
+ const previousRoutePath = lastRenderedRoutePath;
269
434
  const { main, panel } = resolveView(state);
270
- const isLockedRoute = ["editor", "editorResults", "data", "structure"].includes(
435
+ const isLockedRoute = ["editor", "editorResults", "data", "charts", "structure", "tableDesigner"].includes(
271
436
  state.route.name
272
437
  );
438
+ const isEnteringNewTableDesignerRoute =
439
+ state.route.name === "tableDesigner" &&
440
+ state.route.params?.isNew &&
441
+ previousRoutePath !== state.route.path;
442
+
443
+ if (isEnteringNewTableDesignerRoute) {
444
+ pendingNewTableDesignerAutofocus = true;
445
+ } else if (state.route.name !== "tableDesigner" || !state.route.params?.isNew) {
446
+ pendingNewTableDesignerAutofocus = false;
447
+ }
273
448
 
274
449
  teardownStructureGraph();
450
+ teardownQueryChartRenderer();
275
451
  shellRefs.topNav.innerHTML = renderTopNav(state);
276
452
  shellRefs.sidebar.innerHTML = renderSidebar(state);
277
453
  shellRefs.statusBar.innerHTML = renderStatusBar(state);
@@ -281,13 +457,31 @@ function renderApp(state) {
281
457
  shellRefs.modal.innerHTML = renderModal(state);
282
458
  shellRefs.toast.innerHTML = renderToasts(state.toasts);
283
459
  shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
284
- restoreFocusedInputState(focusedInput);
460
+
461
+ if (
462
+ pendingNewTableDesignerAutofocus &&
463
+ state.route.name === "tableDesigner" &&
464
+ state.route.params?.isNew &&
465
+ state.tableDesigner.draft?.mode === "create"
466
+ ) {
467
+ if (focusNewTableDesignerNameField()) {
468
+ pendingNewTableDesignerAutofocus = false;
469
+ }
470
+ } else {
471
+ restoreFocusedInputState(focusedInput);
472
+ }
473
+
474
+ lastRenderedRoutePath = state.route.path;
285
475
 
286
476
  if (state.route.name === "structure") {
287
477
  mountStructureGraph(state).catch((error) => {
288
478
  console.error("Failed to mount structure graph.", error);
289
479
  });
290
480
  }
481
+
482
+ if (state.route.name === "charts") {
483
+ mountQueryChartRenderer(state);
484
+ }
291
485
  }
292
486
 
293
487
  const router = createRouter((route) => {
@@ -312,6 +506,15 @@ async function handleAction(actionNode) {
312
506
  case "open-modal":
313
507
  openModal(actionNode.dataset.modal);
314
508
  return;
509
+ case "open-create-query-chart-modal":
510
+ openCreateQueryChartModal();
511
+ return;
512
+ case "open-edit-query-chart-modal":
513
+ openEditQueryChartModal(actionNode.dataset.chartId);
514
+ return;
515
+ case "open-delete-query-chart-modal":
516
+ openDeleteQueryChartModal(actionNode.dataset.chartId);
517
+ return;
315
518
  case "edit-connection":
316
519
  openEditConnectionModal(actionNode.dataset.connectionId);
317
520
  return;
@@ -386,6 +589,16 @@ async function handleAction(actionNode) {
386
589
  await setQueryHistoryTab(actionNode.dataset.tab);
387
590
  }
388
591
  return;
592
+ case "toggle-query-history-panel":
593
+ setQueryHistoryPanelVisibility(
594
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
595
+ );
596
+ return;
597
+ case "toggle-editor-panel":
598
+ setEditorPanelVisibility(
599
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
600
+ );
601
+ return;
389
602
  case "load-more-query-history":
390
603
  await loadMoreQueryHistory();
391
604
  return;
@@ -408,6 +621,22 @@ async function handleAction(actionNode) {
408
621
  );
409
622
  }
410
623
  return;
624
+ case "toggle-charts-sql-panel":
625
+ toggleChartsSqlPanel();
626
+ return;
627
+ case "toggle-charts-results-panel":
628
+ toggleChartsResultsPanel();
629
+ return;
630
+ case "set-charts-height-preset":
631
+ if (actionNode.dataset.preset) {
632
+ setChartsHeightPreset(actionNode.dataset.preset);
633
+ }
634
+ return;
635
+ case "export-query-chart-png":
636
+ if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
637
+ showToast("The selected chart is not ready for PNG export.", "alert");
638
+ }
639
+ return;
411
640
  case "delete-query-history":
412
641
  if (
413
642
  actionNode.dataset.historyId &&
@@ -431,11 +660,61 @@ async function handleAction(actionNode) {
431
660
  case "export-data-csv":
432
661
  await exportCurrentDataTableCsv();
433
662
  return;
663
+ case "toggle-data-tables":
664
+ toggleDataTablesPanel();
665
+ return;
434
666
  case "select-structure-entry":
435
667
  if (actionNode.dataset.entryName) {
436
668
  await selectStructureEntry(actionNode.dataset.entryName);
437
669
  }
438
670
  return;
671
+ case "add-table-designer-column":
672
+ {
673
+ const columnId = addCurrentTableDesignerColumn();
674
+ if (columnId) {
675
+ window.requestAnimationFrame(() => {
676
+ focusTableDesignerColumnNameField(columnId);
677
+ });
678
+ }
679
+ }
680
+ return;
681
+ case "remove-table-designer-column":
682
+ if (actionNode.dataset.columnId) {
683
+ removeCurrentTableDesignerColumn(actionNode.dataset.columnId);
684
+ }
685
+ return;
686
+ case "save-table-designer": {
687
+ const savedTableName = await saveCurrentTableDesignerDraft();
688
+ if (savedTableName) {
689
+ router.navigate(`/table-designer/${encodeURIComponent(savedTableName)}`);
690
+ }
691
+ return;
692
+ }
693
+ case "copy-table-designer-sql": {
694
+ const sqlPreview = getState().tableDesigner.draft?.sqlPreview ?? "";
695
+ if (!sqlPreview.trim()) {
696
+ return;
697
+ }
698
+
699
+ try {
700
+ await navigator.clipboard.writeText(sqlPreview);
701
+ showToast("SQL preview copied.", "success");
702
+ } catch (error) {
703
+ showToast("Clipboard access failed.", "alert");
704
+ }
705
+ return;
706
+ }
707
+ case "import-table-designer-csv": {
708
+ const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
709
+
710
+ if (!(fileInput instanceof HTMLInputElement)) {
711
+ return;
712
+ }
713
+
714
+ fileInput.value = "";
715
+ fileInput.click();
716
+ return;
717
+ }
439
718
  case "select-data-row":
440
719
  if (actionNode.dataset.rowIndex) {
441
720
  selectDataRow(actionNode.dataset.rowIndex);
@@ -554,8 +833,36 @@ document.addEventListener("input", (event) => {
554
833
  return;
555
834
  }
556
835
 
836
+ if (bindNode.dataset.bind === "table-designer-search") {
837
+ setTableDesignerSearchQuery(bindNode.value);
838
+ return;
839
+ }
840
+
841
+ if (bindNode.dataset.bind === "table-designer-field") {
842
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
843
+ return;
844
+ }
845
+
846
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
847
+ return;
848
+ }
849
+
850
+ if (bindNode.dataset.bind === "table-designer-column-field") {
851
+ updateCurrentTableDesignerColumnField(
852
+ bindNode.dataset.columnId,
853
+ bindNode.dataset.field,
854
+ bindNode.value
855
+ );
856
+ return;
857
+ }
858
+
557
859
  if (bindNode.dataset.bind === "query-history-search") {
558
860
  setQueryHistorySearchInput(bindNode.value);
861
+ return;
862
+ }
863
+
864
+ if (bindNode.dataset.bind === "query-chart-draft:name") {
865
+ updateCurrentQueryChartDraftField("name", bindNode.value);
559
866
  }
560
867
  });
561
868
 
@@ -582,6 +889,61 @@ document.addEventListener("change", (event) => {
582
889
 
583
890
  if (bindNode.dataset.bind === "data-search-column") {
584
891
  setDataSearchColumn(bindNode.value);
892
+ return;
893
+ }
894
+
895
+ if (bindNode.dataset.bind === "table-designer-import-file") {
896
+ void handleTableDesignerCsvImport(bindNode);
897
+ return;
898
+ }
899
+
900
+ if (bindNode.dataset.bind === "table-designer-field") {
901
+ if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
902
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
903
+ return;
904
+ }
905
+
906
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
907
+ return;
908
+ }
909
+
910
+ if (bindNode.dataset.bind === "table-designer-column-field") {
911
+ updateCurrentTableDesignerColumnField(
912
+ bindNode.dataset.columnId,
913
+ bindNode.dataset.field,
914
+ bindNode.value
915
+ );
916
+ return;
917
+ }
918
+
919
+ if (bindNode.dataset.bind === "table-designer-column-flag") {
920
+ updateCurrentTableDesignerColumnField(
921
+ bindNode.dataset.columnId,
922
+ bindNode.dataset.field,
923
+ bindNode.checked
924
+ );
925
+ return;
926
+ }
927
+
928
+ if (bindNode.dataset.bind === "query-chart-draft:chartType") {
929
+ updateCurrentQueryChartDraftField("chartType", bindNode.value);
930
+ return;
931
+ }
932
+
933
+ if (bindNode.dataset.bind === "query-chart-draft:tableVisible") {
934
+ updateCurrentQueryChartDraftField("tableVisible", bindNode.checked);
935
+ return;
936
+ }
937
+
938
+ if (bindNode.dataset.bind?.startsWith("query-chart-draft-config:")) {
939
+ const field = bindNode.dataset.bind.slice("query-chart-draft-config:".length);
940
+ const value =
941
+ bindNode instanceof HTMLInputElement && bindNode.type === "checkbox"
942
+ ? bindNode.checked
943
+ : bindNode.value === ""
944
+ ? null
945
+ : bindNode.value;
946
+ updateCurrentQueryChartDraftConfigField(field, value);
585
947
  }
586
948
  });
587
949
 
@@ -667,6 +1029,12 @@ document.addEventListener("submit", async (event) => {
667
1029
  case "delete-row-confirm":
668
1030
  await submitDeleteRowConfirmation();
669
1031
  return;
1032
+ case "save-query-chart":
1033
+ await saveCurrentQueryChartDraft();
1034
+ return;
1035
+ case "delete-query-chart":
1036
+ await submitDeleteChartConfirmation();
1037
+ return;
670
1038
  case "save-data-row": {
671
1039
  const values = {};
672
1040