sqlite-hub 0.6.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.
@@ -9,6 +9,15 @@ import {
9
9
  updateTableDesignerColumnField,
10
10
  updateTableDesignerDraftField,
11
11
  } from "./utils/tableDesigner.js";
12
+ import {
13
+ analyzeQueryChartResult,
14
+ buildDefaultQueryChartName,
15
+ buildQueryChartResultColumns,
16
+ buildSuggestedChartConfig,
17
+ resolveUniqueQueryChartName,
18
+ suggestQueryChartType,
19
+ validateQueryChartConfig,
20
+ } from "./lib/queryCharts.js";
12
21
 
13
22
  const listeners = new Set();
14
23
  const DEFAULT_SETTINGS = {
@@ -19,6 +28,7 @@ const DEFAULT_SETTINGS = {
19
28
  const DATA_PAGE_SIZES = [25, 50, 100];
20
29
  const QUERY_HISTORY_PAGE_SIZE = 30;
21
30
  const QUERY_HISTORY_RUN_LIMIT = 8;
31
+ const CHART_HEIGHT_PRESETS = new Set(["small", "medium", "large"]);
22
32
  const MISSING_DATABASE_ERROR = {
23
33
  code: "ACTIVE_DATABASE_REQUIRED",
24
34
  message: "No active SQLite database selected.",
@@ -28,6 +38,8 @@ let routeLoadVersion = 0;
28
38
  let queryHistoryLoadVersion = 0;
29
39
  let queryHistoryDetailLoadVersion = 0;
30
40
  let queryHistorySearchTimer = null;
41
+ let chartsLoadVersion = 0;
42
+ let chartsDetailLoadVersion = 0;
31
43
 
32
44
  const state = {
33
45
  ready: false,
@@ -55,6 +67,7 @@ const state = {
55
67
  dataBrowser: {
56
68
  tables: [],
57
69
  selectedTable: null,
70
+ tablesVisible: true,
58
71
  table: null,
59
72
  loading: false,
60
73
  tableLoading: false,
@@ -73,7 +86,9 @@ const state = {
73
86
  },
74
87
  editor: {
75
88
  sqlText: "",
89
+ editorPanelVisible: true,
76
90
  history: [],
91
+ historyPanelVisible: true,
77
92
  historyLoading: false,
78
93
  historyLoadingMore: false,
79
94
  historyError: null,
@@ -101,6 +116,21 @@ const state = {
101
116
  deleting: false,
102
117
  saveError: null,
103
118
  },
119
+ charts: {
120
+ queries: [],
121
+ loading: false,
122
+ error: null,
123
+ selectedHistoryId: null,
124
+ chartHeightPreset: "medium",
125
+ sqlExpanded: false,
126
+ resultsVisible: true,
127
+ detail: null,
128
+ detailLoading: false,
129
+ detailError: null,
130
+ result: null,
131
+ resultLoading: false,
132
+ resultError: null,
133
+ },
104
134
  tableDesigner: {
105
135
  tables: [],
106
136
  selectedTableName: null,
@@ -172,6 +202,7 @@ function requiresActiveDatabase(routeName) {
172
202
  "data",
173
203
  "editor",
174
204
  "editorResults",
205
+ "charts",
175
206
  "structure",
176
207
  "tableDesigner",
177
208
  ].includes(routeName);
@@ -375,6 +406,52 @@ function resetQueryHistoryState({ preserveSearch = true } = {}) {
375
406
  }
376
407
  }
377
408
 
409
+ function resetChartsState() {
410
+ chartsLoadVersion += 1;
411
+ chartsDetailLoadVersion += 1;
412
+ state.charts.queries = [];
413
+ state.charts.loading = false;
414
+ state.charts.error = null;
415
+ state.charts.selectedHistoryId = null;
416
+ state.charts.chartHeightPreset = "medium";
417
+ state.charts.sqlExpanded = false;
418
+ state.charts.resultsVisible = true;
419
+ state.charts.detail = null;
420
+ state.charts.detailLoading = false;
421
+ state.charts.detailError = null;
422
+ state.charts.result = null;
423
+ state.charts.resultLoading = false;
424
+ state.charts.resultError = null;
425
+ }
426
+
427
+ function normalizeChartsHeightPreset(value) {
428
+ const normalizedValue = String(value ?? "")
429
+ .trim()
430
+ .toLowerCase();
431
+
432
+ return CHART_HEIGHT_PRESETS.has(normalizedValue) ? normalizedValue : "medium";
433
+ }
434
+
435
+ function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
436
+ if (!updatedItem) {
437
+ return updatedItem;
438
+ }
439
+
440
+ return {
441
+ ...(fallbackItem ?? {}),
442
+ ...updatedItem,
443
+ chartCount:
444
+ updatedItem.chartCount !== undefined
445
+ ? Number(updatedItem.chartCount ?? 0)
446
+ : Number(fallbackItem?.chartCount ?? 0),
447
+ chartTypes: Array.isArray(updatedItem.chartTypes)
448
+ ? [...updatedItem.chartTypes]
449
+ : Array.isArray(fallbackItem?.chartTypes)
450
+ ? [...fallbackItem.chartTypes]
451
+ : [],
452
+ };
453
+ }
454
+
378
455
  function syncQueryHistoryItem(updatedItem) {
379
456
  if (!updatedItem) {
380
457
  return;
@@ -387,6 +464,61 @@ function syncQueryHistoryItem(updatedItem) {
387
464
  if (state.editor.historyDetail?.id === updatedItem.id) {
388
465
  state.editor.historyDetail = updatedItem;
389
466
  }
467
+
468
+ const existingChartsEntry =
469
+ state.charts.queries.find((entry) => entry.id === updatedItem.id) ??
470
+ (state.charts.detail?.item?.id === updatedItem.id ? state.charts.detail.item : null);
471
+ const mergedChartsItem = mergeQueryHistoryItemWithChartSummary(updatedItem, existingChartsEntry);
472
+
473
+ state.charts.queries = state.charts.queries.map((entry) =>
474
+ entry.id === updatedItem.id ? mergedChartsItem : entry
475
+ );
476
+
477
+ if (state.charts.detail?.item?.id === updatedItem.id) {
478
+ state.charts.detail = {
479
+ ...state.charts.detail,
480
+ item: mergeQueryHistoryItemWithChartSummary(updatedItem, state.charts.detail.item),
481
+ };
482
+ }
483
+ }
484
+
485
+ function syncChartsQuerySummaryForHistory(historyId) {
486
+ const numericId = Number(historyId);
487
+
488
+ if (!Number.isInteger(numericId) || numericId < 1 || state.charts.detail?.item?.id !== numericId) {
489
+ return;
490
+ }
491
+
492
+ const charts = state.charts.detail?.charts ?? [];
493
+ const chartTypes = [...new Set(
494
+ charts
495
+ .map((chart) =>
496
+ String(chart?.chartType ?? "")
497
+ .trim()
498
+ .toLowerCase()
499
+ )
500
+ .filter(Boolean)
501
+ )];
502
+ const chartCount = charts.length;
503
+
504
+ state.charts.queries = state.charts.queries.map((entry) =>
505
+ entry.id === numericId
506
+ ? {
507
+ ...entry,
508
+ chartCount,
509
+ chartTypes,
510
+ }
511
+ : entry
512
+ );
513
+
514
+ state.charts.detail = {
515
+ ...state.charts.detail,
516
+ item: {
517
+ ...state.charts.detail.item,
518
+ chartCount,
519
+ chartTypes,
520
+ },
521
+ };
390
522
  }
391
523
 
392
524
  function resolveQueryHistorySql(historyId) {
@@ -396,6 +528,10 @@ function resolveQueryHistorySql(historyId) {
396
528
  return state.editor.historyDetail.rawSql;
397
529
  }
398
530
 
531
+ if (String(state.charts.detail?.item?.id ?? "") === historyIdAsString) {
532
+ return state.charts.detail.item.rawSql;
533
+ }
534
+
399
535
  return findQueryHistoryItem(historyId)?.rawSql ?? null;
400
536
  }
401
537
 
@@ -403,6 +539,9 @@ function clearRouteSlices() {
403
539
  state.overview.error = null;
404
540
  state.dataBrowser.error = null;
405
541
  state.dataBrowser.saveError = null;
542
+ state.charts.error = null;
543
+ state.charts.detailError = null;
544
+ state.charts.resultError = null;
406
545
  state.tableDesigner.error = null;
407
546
  state.tableDesigner.saveError = null;
408
547
  state.structure.error = null;
@@ -446,6 +585,9 @@ function setMissingDatabaseState() {
446
585
  state.tableDesigner.error = error;
447
586
  state.tableDesigner.saveError = null;
448
587
 
588
+ resetChartsState();
589
+ state.charts.error = error;
590
+
449
591
  resetQueryHistoryState({ preserveSearch: false });
450
592
  }
451
593
 
@@ -658,6 +800,114 @@ async function refreshQueryHistoryState({ append = false } = {}) {
658
800
  }
659
801
  }
660
802
 
803
+ async function loadChartsDetail(historyId) {
804
+ const numericId = Number(historyId);
805
+ const requestVersion = ++chartsDetailLoadVersion;
806
+
807
+ if (!Number.isInteger(numericId) || numericId < 1) {
808
+ state.charts.selectedHistoryId = null;
809
+ state.charts.sqlExpanded = false;
810
+ state.charts.resultsVisible = true;
811
+ state.charts.detail = null;
812
+ state.charts.detailLoading = false;
813
+ state.charts.detailError = null;
814
+ state.charts.result = null;
815
+ state.charts.resultLoading = false;
816
+ state.charts.resultError = null;
817
+ emitChange();
818
+ return;
819
+ }
820
+
821
+ state.charts.selectedHistoryId = numericId;
822
+ state.charts.sqlExpanded = false;
823
+ state.charts.resultsVisible = true;
824
+ state.charts.detail = null;
825
+ state.charts.detailLoading = true;
826
+ state.charts.detailError = null;
827
+ state.charts.result = null;
828
+ state.charts.resultLoading = true;
829
+ state.charts.resultError = null;
830
+ emitChange();
831
+
832
+ const [detailResponse, resultResponse] = await Promise.allSettled([
833
+ api.getChartsQueryHistoryDetail(numericId),
834
+ api.executeChartsQueryHistory(numericId),
835
+ ]);
836
+
837
+ if (
838
+ requestVersion !== chartsDetailLoadVersion ||
839
+ state.charts.selectedHistoryId !== numericId
840
+ ) {
841
+ return;
842
+ }
843
+
844
+ if (detailResponse.status === "fulfilled") {
845
+ state.charts.detail = detailResponse.value.data ?? null;
846
+ state.charts.detailError = null;
847
+ if (detailResponse.value.data?.item) {
848
+ syncQueryHistoryItem(detailResponse.value.data.item);
849
+ syncChartsQuerySummaryForHistory(detailResponse.value.data.item.id);
850
+ }
851
+ } else {
852
+ state.charts.detail = null;
853
+ state.charts.detailError = normalizeError(detailResponse.reason);
854
+ }
855
+
856
+ if (resultResponse.status === "fulfilled") {
857
+ state.charts.result = resultResponse.value.data ?? null;
858
+ state.charts.resultError = null;
859
+ } else {
860
+ state.charts.result = null;
861
+ state.charts.resultError = normalizeError(resultResponse.reason);
862
+ }
863
+
864
+ if (requestVersion === chartsDetailLoadVersion) {
865
+ state.charts.detailLoading = false;
866
+ state.charts.resultLoading = false;
867
+ emitChange();
868
+ }
869
+ }
870
+
871
+ async function loadCharts(version, route) {
872
+ state.charts.loading = true;
873
+ state.charts.error = null;
874
+ emitChange();
875
+
876
+ try {
877
+ const response = await api.getChartsQueryHistory();
878
+
879
+ if (version !== routeLoadVersion) {
880
+ return;
881
+ }
882
+
883
+ state.charts.queries = response.data ?? [];
884
+ state.charts.error = null;
885
+ } catch (error) {
886
+ if (version !== routeLoadVersion) {
887
+ return;
888
+ }
889
+
890
+ state.charts.queries = [];
891
+ state.charts.error = normalizeError(error);
892
+ } finally {
893
+ if (version === routeLoadVersion) {
894
+ state.charts.loading = false;
895
+ emitChange();
896
+ }
897
+ }
898
+
899
+ if (version !== routeLoadVersion) {
900
+ return;
901
+ }
902
+
903
+ const requestedHistoryId = Number(route.params?.historyId ?? null);
904
+ const canLoadRequestedHistory =
905
+ Number.isInteger(requestedHistoryId) &&
906
+ state.charts.queries.some((item) => item.id === requestedHistoryId);
907
+
908
+ await loadChartsDetail(canLoadRequestedHistory ? requestedHistoryId : null);
909
+ }
910
+
661
911
  async function loadOverview(version) {
662
912
  state.overview.loading = true;
663
913
  state.overview.error = null;
@@ -1004,6 +1254,7 @@ function invalidateDatabaseCaches() {
1004
1254
  state.tableDesigner.supportedTypes = [];
1005
1255
  state.tableDesigner.error = null;
1006
1256
  state.tableDesigner.saveError = null;
1257
+ resetChartsState();
1007
1258
  state.structure.data = null;
1008
1259
  state.structure.detail = null;
1009
1260
  }
@@ -1031,6 +1282,9 @@ async function loadRouteData(route) {
1031
1282
  case "data":
1032
1283
  await loadData(version, route);
1033
1284
  return;
1285
+ case "charts":
1286
+ await loadCharts(version, route);
1287
+ return;
1034
1288
  case "editor":
1035
1289
  case "editorResults":
1036
1290
  await refreshQueryHistoryState();
@@ -1083,6 +1337,94 @@ function closeModalInternal() {
1083
1337
  emitChange();
1084
1338
  }
1085
1339
 
1340
+ function getChartsResultAnalysis(snapshot = state) {
1341
+ return snapshot.charts.result ? analyzeQueryChartResult(snapshot.charts.result) : null;
1342
+ }
1343
+
1344
+ function buildQueryChartDraft(mode, chart = null) {
1345
+ const queryItem = state.charts.detail?.item ?? null;
1346
+ const analysis = getChartsResultAnalysis();
1347
+
1348
+ if (!queryItem || !analysis) {
1349
+ return null;
1350
+ }
1351
+
1352
+ const existingCharts = state.charts.detail?.charts ?? [];
1353
+
1354
+ if (mode === "edit" && chart) {
1355
+ return {
1356
+ mode,
1357
+ chartId: chart.id,
1358
+ queryHistoryId: chart.queryHistoryId,
1359
+ chartType: chart.chartType,
1360
+ name: chart.name,
1361
+ nameTouched: true,
1362
+ config: structuredClone(chart.config),
1363
+ tableVisible: chart.tableVisible,
1364
+ resultColumns: buildQueryChartResultColumns(analysis),
1365
+ };
1366
+ }
1367
+
1368
+ const chartType = suggestQueryChartType(analysis);
1369
+ const name = resolveUniqueQueryChartName(
1370
+ buildDefaultQueryChartName(chartType, queryItem.displayTitle),
1371
+ existingCharts
1372
+ );
1373
+
1374
+ return {
1375
+ mode: "create",
1376
+ chartId: null,
1377
+ queryHistoryId: queryItem.id,
1378
+ chartType,
1379
+ name,
1380
+ nameTouched: false,
1381
+ config: buildSuggestedChartConfig(chartType, analysis),
1382
+ tableVisible: true,
1383
+ resultColumns: buildQueryChartResultColumns(analysis),
1384
+ };
1385
+ }
1386
+
1387
+ function upsertChartDetailItem(updatedChart) {
1388
+ if (!updatedChart || state.charts.detail?.item?.id !== updatedChart.queryHistoryId) {
1389
+ return;
1390
+ }
1391
+
1392
+ const nextCharts = [...(state.charts.detail?.charts ?? [])];
1393
+ const existingIndex = nextCharts.findIndex((chart) => chart.id === updatedChart.id);
1394
+
1395
+ if (existingIndex >= 0) {
1396
+ nextCharts.splice(existingIndex, 1, updatedChart);
1397
+ } else {
1398
+ nextCharts.push(updatedChart);
1399
+ }
1400
+
1401
+ nextCharts.sort((left, right) => {
1402
+ const leftTime = Date.parse(left.createdAt ?? "") || 0;
1403
+ const rightTime = Date.parse(right.createdAt ?? "") || 0;
1404
+ return leftTime - rightTime || left.id - right.id;
1405
+ });
1406
+
1407
+ state.charts.detail = {
1408
+ ...state.charts.detail,
1409
+ charts: nextCharts,
1410
+ };
1411
+
1412
+ syncChartsQuerySummaryForHistory(updatedChart.queryHistoryId);
1413
+ }
1414
+
1415
+ function removeChartDetailItem(chartId) {
1416
+ if (!state.charts.detail) {
1417
+ return;
1418
+ }
1419
+
1420
+ state.charts.detail = {
1421
+ ...state.charts.detail,
1422
+ charts: (state.charts.detail.charts ?? []).filter((chart) => chart.id !== Number(chartId)),
1423
+ };
1424
+
1425
+ syncChartsQuerySummaryForHistory(state.charts.detail.item?.id);
1426
+ }
1427
+
1086
1428
  export function getState() {
1087
1429
  return clone(state);
1088
1430
  }
@@ -1205,10 +1547,147 @@ export function openDeleteEditorRowModal(rowIndex) {
1205
1547
  emitChange();
1206
1548
  }
1207
1549
 
1550
+ export function openCreateQueryChartModal() {
1551
+ const draft = buildQueryChartDraft("create");
1552
+
1553
+ if (!state.charts.detail?.item || !state.charts.result) {
1554
+ pushToast("The selected query has no chartable result set yet.", "alert");
1555
+ return;
1556
+ }
1557
+
1558
+ if (!draft) {
1559
+ pushToast("The chart editor could not be opened for this query.", "alert");
1560
+ return;
1561
+ }
1562
+
1563
+ state.modal = {
1564
+ kind: "chart-editor",
1565
+ error: null,
1566
+ submitting: false,
1567
+ draft,
1568
+ };
1569
+ emitChange();
1570
+ }
1571
+
1572
+ export function openEditQueryChartModal(chartId) {
1573
+ if (!state.charts.result) {
1574
+ pushToast("Reload the query result before editing this chart.", "alert");
1575
+ return;
1576
+ }
1577
+
1578
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1579
+ const draft = chart ? buildQueryChartDraft("edit", chart) : null;
1580
+
1581
+ if (!chart || !draft) {
1582
+ pushToast("The selected chart could not be loaded.", "alert");
1583
+ return;
1584
+ }
1585
+
1586
+ state.modal = {
1587
+ kind: "chart-editor",
1588
+ error: null,
1589
+ submitting: false,
1590
+ draft,
1591
+ };
1592
+ emitChange();
1593
+ }
1594
+
1595
+ export function openDeleteQueryChartModal(chartId) {
1596
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
1597
+
1598
+ if (!chart) {
1599
+ pushToast("The selected chart could not be loaded.", "alert");
1600
+ return;
1601
+ }
1602
+
1603
+ state.modal = {
1604
+ kind: "delete-chart",
1605
+ chartId: chart.id,
1606
+ chartName: chart.name,
1607
+ error: null,
1608
+ submitting: false,
1609
+ };
1610
+ emitChange();
1611
+ }
1612
+
1208
1613
  export function closeModal() {
1209
1614
  closeModalInternal();
1210
1615
  }
1211
1616
 
1617
+ export function toggleChartsSqlPanel() {
1618
+ state.charts.sqlExpanded = !state.charts.sqlExpanded;
1619
+ emitChange();
1620
+ }
1621
+
1622
+ export function toggleChartsResultsPanel() {
1623
+ state.charts.resultsVisible = !state.charts.resultsVisible;
1624
+ emitChange();
1625
+ }
1626
+
1627
+ export function setChartsHeightPreset(preset) {
1628
+ const nextPreset = normalizeChartsHeightPreset(preset);
1629
+
1630
+ if (state.charts.chartHeightPreset === nextPreset) {
1631
+ return;
1632
+ }
1633
+
1634
+ state.charts.chartHeightPreset = nextPreset;
1635
+ emitChange();
1636
+ }
1637
+
1638
+ export function updateCurrentQueryChartDraftField(field, value) {
1639
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1640
+ return;
1641
+ }
1642
+
1643
+ state.modal.error = null;
1644
+
1645
+ if (field === "name") {
1646
+ state.modal.draft.name = String(value ?? "");
1647
+ state.modal.draft.nameTouched = true;
1648
+ emitChange();
1649
+ return;
1650
+ }
1651
+
1652
+ if (field === "chartType") {
1653
+ const nextType = String(value ?? "").trim().toLowerCase();
1654
+ const analysis = getChartsResultAnalysis();
1655
+
1656
+ state.modal.draft.chartType = nextType;
1657
+ state.modal.draft.config = buildSuggestedChartConfig(nextType, analysis);
1658
+
1659
+ if (!state.modal.draft.nameTouched) {
1660
+ const existingCharts = state.charts.detail?.charts ?? [];
1661
+ state.modal.draft.name = resolveUniqueQueryChartName(
1662
+ buildDefaultQueryChartName(nextType, state.charts.detail?.item?.displayTitle),
1663
+ existingCharts,
1664
+ state.modal.draft.chartId
1665
+ );
1666
+ }
1667
+
1668
+ emitChange();
1669
+ return;
1670
+ }
1671
+
1672
+ if (field === "tableVisible") {
1673
+ state.modal.draft.tableVisible = Boolean(value);
1674
+ emitChange();
1675
+ }
1676
+ }
1677
+
1678
+ export function updateCurrentQueryChartDraftConfigField(field, value) {
1679
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
1680
+ return;
1681
+ }
1682
+
1683
+ state.modal.error = null;
1684
+ state.modal.draft.config = {
1685
+ ...state.modal.draft.config,
1686
+ [field]: value,
1687
+ };
1688
+ emitChange();
1689
+ }
1690
+
1212
1691
  export function dismissToast(id) {
1213
1692
  const nextToasts = state.toasts.filter((toast) => toast.id !== id);
1214
1693
 
@@ -1408,6 +1887,18 @@ export function clearEditorResults() {
1408
1887
  emitChange();
1409
1888
  }
1410
1889
 
1890
+ export function setEditorPanelVisibility(visible) {
1891
+ const nextValue =
1892
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.editorPanelVisible);
1893
+
1894
+ if (state.editor.editorPanelVisible === nextValue) {
1895
+ return;
1896
+ }
1897
+
1898
+ state.editor.editorPanelVisible = nextValue;
1899
+ emitChange();
1900
+ }
1901
+
1411
1902
  export function setEditorTab(tab) {
1412
1903
  state.editor.activeTab = tab;
1413
1904
  emitChange();
@@ -1478,6 +1969,18 @@ export async function setQueryHistoryTab(tab) {
1478
1969
  await refreshQueryHistoryState();
1479
1970
  }
1480
1971
 
1972
+ export function setQueryHistoryPanelVisibility(visible) {
1973
+ const nextValue =
1974
+ typeof visible === "boolean" ? visible : !Boolean(state.editor.historyPanelVisible);
1975
+
1976
+ if (state.editor.historyPanelVisible === nextValue) {
1977
+ return;
1978
+ }
1979
+
1980
+ state.editor.historyPanelVisible = nextValue;
1981
+ emitChange();
1982
+ }
1983
+
1481
1984
  export function setQueryHistorySearchInput(query) {
1482
1985
  state.editor.historySearchInput = String(query ?? "");
1483
1986
  emitChange();
@@ -1799,6 +2302,11 @@ export function setDataSearchColumn(columnName) {
1799
2302
  emitChange();
1800
2303
  }
1801
2304
 
2305
+ export function toggleDataTablesPanel() {
2306
+ state.dataBrowser.tablesVisible = state.dataBrowser.tablesVisible === false;
2307
+ emitChange();
2308
+ }
2309
+
1802
2310
  export async function setDataPage(page) {
1803
2311
  const numericPage = Number(page);
1804
2312
 
@@ -2061,6 +2569,116 @@ export async function submitDeleteRowConfirmation() {
2061
2569
  return result;
2062
2570
  }
2063
2571
 
2572
+ export async function saveCurrentQueryChartDraft() {
2573
+ if (state.modal?.kind !== "chart-editor" || !state.modal.draft) {
2574
+ return null;
2575
+ }
2576
+
2577
+ const analysis = getChartsResultAnalysis();
2578
+
2579
+ if (!analysis) {
2580
+ state.modal.error = {
2581
+ code: "RESULT_SET_REQUIRED",
2582
+ message: "Reload the query result before saving a chart.",
2583
+ };
2584
+ emitChange();
2585
+ return null;
2586
+ }
2587
+
2588
+ const validation = validateQueryChartConfig(
2589
+ state.modal.draft.chartType,
2590
+ state.modal.draft.config,
2591
+ analysis
2592
+ );
2593
+
2594
+ if (!validation.valid) {
2595
+ state.modal.error = {
2596
+ code: "VALIDATION_ERROR",
2597
+ message: validation.errors.join(" "),
2598
+ };
2599
+ emitChange();
2600
+ return null;
2601
+ }
2602
+
2603
+ startModalSubmission();
2604
+ const draftMode = state.modal.draft.mode;
2605
+
2606
+ const payload = {
2607
+ queryHistoryId: state.modal.draft.queryHistoryId,
2608
+ name: state.modal.draft.name,
2609
+ chartType: state.modal.draft.chartType,
2610
+ config: state.modal.draft.config,
2611
+ resultColumns: buildQueryChartResultColumns(analysis),
2612
+ tableVisible: state.modal.draft.tableVisible,
2613
+ };
2614
+
2615
+ try {
2616
+ const response =
2617
+ state.modal.draft.mode === "edit"
2618
+ ? await api.updateQueryHistoryChart(state.modal.draft.chartId, payload)
2619
+ : await api.createQueryHistoryChart(payload);
2620
+ const chart = response.data ?? null;
2621
+
2622
+ if (chart) {
2623
+ upsertChartDetailItem(chart);
2624
+ closeModalInternal();
2625
+ pushToast(response.message || (draftMode === "edit" ? "Chart updated." : "Chart created."), "success");
2626
+ return chart;
2627
+ }
2628
+
2629
+ closeModalInternal();
2630
+ return null;
2631
+ } catch (error) {
2632
+ withModalError(error);
2633
+ return null;
2634
+ }
2635
+ }
2636
+
2637
+ export async function toggleQueryChartTableVisibility(chartId) {
2638
+ const chart = state.charts.detail?.charts?.find((entry) => entry.id === Number(chartId)) ?? null;
2639
+
2640
+ if (!chart) {
2641
+ pushToast("The selected chart could not be loaded.", "alert");
2642
+ return null;
2643
+ }
2644
+
2645
+ try {
2646
+ const response = await api.updateQueryHistoryChart(chart.id, {
2647
+ tableVisible: !chart.tableVisible,
2648
+ });
2649
+
2650
+ upsertChartDetailItem(response.data ?? null);
2651
+ emitChange();
2652
+ return response.data ?? null;
2653
+ } catch (error) {
2654
+ pushToast(normalizeError(error)?.message || "Chart update failed.", "alert");
2655
+ return null;
2656
+ }
2657
+ }
2658
+
2659
+ export async function deleteQueryChart(chartId) {
2660
+ try {
2661
+ const response = await api.deleteQueryHistoryChart(chartId);
2662
+ removeChartDetailItem(chartId);
2663
+ closeModalInternal();
2664
+ pushToast(response.message || "Chart deleted.", "muted");
2665
+ emitChange();
2666
+ return true;
2667
+ } catch (error) {
2668
+ withModalError(error);
2669
+ return false;
2670
+ }
2671
+ }
2672
+
2673
+ export async function submitDeleteChartConfirmation() {
2674
+ if (state.modal?.kind !== "delete-chart") {
2675
+ return false;
2676
+ }
2677
+
2678
+ startModalSubmission();
2679
+ return deleteQueryChart(state.modal.chartId);
2680
+ }
2681
+
2064
2682
  export async function exportCurrentQueryCsv() {
2065
2683
  state.editor.exportLoading = true;
2066
2684
  emitChange();