sqlite-hub 0.17.2 → 1.1.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 (85) hide show
  1. package/README.md +143 -47
  2. package/bin/sqlite-hub.js +206 -444
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +263 -34
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +77 -21
  30. package/frontend/js/components/queryChartRenderer.js +28 -3
  31. package/frontend/js/components/queryEditor.js +20 -24
  32. package/frontend/js/components/queryHistoryDetail.js +1 -1
  33. package/frontend/js/components/queryHistoryHeader.js +48 -0
  34. package/frontend/js/components/queryHistoryList.js +132 -0
  35. package/frontend/js/components/queryHistoryPanel.js +72 -136
  36. package/frontend/js/components/structureGraph.js +699 -89
  37. package/frontend/js/components/tableDesignerEditor.js +3 -5
  38. package/frontend/js/store.js +188 -7
  39. package/frontend/js/utils/exportFilenames.js +3 -1
  40. package/frontend/js/views/charts.js +320 -169
  41. package/frontend/js/views/connections.js +59 -64
  42. package/frontend/js/views/data.js +12 -20
  43. package/frontend/js/views/editor.js +0 -2
  44. package/frontend/js/views/settings.js +219 -5
  45. package/frontend/js/views/structure.js +27 -13
  46. package/frontend/styles/components.css +155 -0
  47. package/frontend/styles/structure-graph.css +140 -35
  48. package/frontend/styles/tailwind.generated.css +2 -2
  49. package/frontend/styles/views.css +12 -6
  50. package/package.json +7 -6
  51. package/server/middleware/apiTokenAuth.js +30 -0
  52. package/server/routes/connections.js +17 -0
  53. package/server/routes/export.js +89 -22
  54. package/server/routes/externalApi.js +304 -0
  55. package/server/routes/settings.js +90 -21
  56. package/server/server.js +22 -1
  57. package/server/services/apiTokenService.js +101 -0
  58. package/server/services/appInfoService.js +215 -0
  59. package/server/services/databaseCommandService.js +443 -0
  60. package/server/services/nativeFileDialogService.js +93 -1
  61. package/server/services/sqlite/exportService.js +307 -22
  62. package/server/services/storage/appStateStore.js +113 -0
  63. package/server/utils/errors.js +7 -0
  64. package/tests/api-token-auth.test.js +236 -0
  65. package/tests/cli-args.test.js +16 -3
  66. package/tests/cli-service-delegation.test.js +43 -0
  67. package/tests/connections-file-dialog-route.test.js +43 -0
  68. package/tests/copy-column-modal.test.js +34 -0
  69. package/tests/database-command-service.test.js +139 -0
  70. package/tests/export-blob.test.js +54 -1
  71. package/tests/export-filenames.test.js +4 -0
  72. package/tests/form-controls.test.js +34 -0
  73. package/tests/native-file-dialog.test.js +27 -0
  74. package/tests/settings-api-tokens-route.test.js +97 -0
  75. package/tests/settings-metadata.test.js +99 -1
  76. package/tests/settings-view.test.js +75 -0
  77. package/frontend/assets/mockups/connections.png +0 -0
  78. package/frontend/assets/mockups/data.png +0 -0
  79. package/frontend/assets/mockups/data_row_editor.png +0 -0
  80. package/frontend/assets/mockups/home.png +0 -0
  81. package/frontend/assets/mockups/sql_editor.png +0 -0
  82. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  83. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  84. package/frontend/assets/mockups/structure.png +0 -0
  85. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -0,0 +1,31 @@
1
+ const SQLITE_HUB_URL = 'http://127.0.0.1:4180';
2
+ // Create a new token for your database in Settings > API Tokens.
3
+ const apiToken = 'shub_Lv9A2xocv01PqT6a0jEmuT-PxPyqfzI_UBaY_VEzjqE';
4
+ const DATABASE_ID = 'conn_ae9b5e54ae8eca1d';
5
+ const path = `${SQLITE_HUB_URL}/api/v1/databases/${DATABASE_ID}`;
6
+
7
+ async function databaseInfo(path, { method = 'GET' } = {}) {
8
+ console.log(path);
9
+ const response = await fetch(path, {
10
+ method,
11
+ headers: {
12
+ Accept: 'application/json',
13
+ Authorization: `Bearer ${apiToken}`,
14
+ },
15
+ });
16
+ return await response.json();
17
+ }
18
+
19
+ const queries = await databaseInfo(`${path}/queries`);
20
+
21
+ console.log(queries.data.items.map(query => query.displayTitle));
22
+
23
+ const queryName = encodeURIComponent(queries.data.items[0].displayTitle);
24
+ const query = await databaseInfo(`${path}/queries/${queryName}`);
25
+
26
+ console.log(query.data);
27
+
28
+ // Executing a saved query changes server state and therefore requires POST.
29
+ const exec = await databaseInfo(`${path}/queries/${queryName}/execute`, { method: 'POST' });
30
+
31
+ console.log(exec.data);
@@ -0,0 +1,27 @@
1
+ const SQLITE_HUB_URL = 'http://127.0.0.1:4180';
2
+ // Create a new token for your database in Settings > API Tokens.
3
+ const apiToken = 'shub_Lv9A2xocv01PqT6a0jEmuT-PxPyqfzI_UBaY_VEzjqE';
4
+ const DATABASE_ID = 'conn_ae9b5e54ae8eca1d';
5
+ const path = `${SQLITE_HUB_URL}/api/v1/databases/${DATABASE_ID}`;
6
+
7
+ async function databaseInfo(path) {
8
+ console.log(path);
9
+ const response = await fetch(path, {
10
+ headers: {
11
+ Accept: 'application/json',
12
+ Authorization: `Bearer ${apiToken}`,
13
+ },
14
+ });
15
+
16
+ return await response.json();
17
+ }
18
+
19
+ const info = await databaseInfo(path);
20
+ const tables = await databaseInfo(`${path}/tables`);
21
+
22
+ console.log(info.data);
23
+ console.log(tables.data.items);
24
+
25
+ const row = await databaseInfo(`${path}/tables/${tables.data.items[0].name}`);
26
+
27
+ console.log(row.data);
@@ -86,6 +86,12 @@ export function createConnection(payload) {
86
86
  });
87
87
  }
88
88
 
89
+ export function chooseOpenDatabasePath() {
90
+ return request("/api/connections/choose-open-path", {
91
+ method: "POST",
92
+ });
93
+ }
94
+
89
95
  export function chooseCreateDatabasePath() {
90
96
  return request("/api/connections/choose-create-path", {
91
97
  method: "POST",
@@ -425,10 +431,29 @@ export function patchSettings(settings) {
425
431
  });
426
432
  }
427
433
 
434
+ export function checkAppVersion() {
435
+ return request("/api/settings/version-check");
436
+ }
437
+
438
+ export function createApiToken(name) {
439
+ return request("/api/settings/api-tokens", {
440
+ method: "POST",
441
+ body: { name },
442
+ });
443
+ }
444
+
445
+ export function deleteApiToken(tokenId) {
446
+ return request(`/api/settings/api-tokens/${encodeURIComponent(tokenId)}`, {
447
+ method: "DELETE",
448
+ });
449
+ }
450
+
428
451
  const TEXT_EXPORT_EXTENSIONS = {
429
452
  csv: "csv",
430
453
  tsv: "tsv",
431
454
  md: "md",
455
+ json: "json",
456
+ parquet: "parquet",
432
457
  };
433
458
 
434
459
  function normalizeTextExportFormat(format) {
@@ -24,6 +24,8 @@ import { renderTopNav } from './components/topNav.js';
24
24
  import { createRouter } from './router.js';
25
25
  import {
26
26
  createActiveConnectionBackup,
27
+ createSettingsApiToken,
28
+ chooseOpenDatabasePath,
27
29
  chooseCreateDatabasePath,
28
30
  createDocument,
29
31
  createDocumentFromMarkdownExport,
@@ -33,6 +35,7 @@ import {
33
35
  clearEditorResults,
34
36
  clearQueryHistorySelection,
35
37
  closeModal,
38
+ checkSettingsAppVersion,
36
39
  dismissMediaTaggingIssue,
37
40
  dismissToast,
38
41
  executeCurrentQuery,
@@ -49,6 +52,7 @@ import {
49
52
  openDeleteDataRowModal,
50
53
  openDeleteEditorRowModal,
51
54
  openDeleteQueryHistoryModal,
55
+ openDeleteSettingsApiTokenModal,
52
56
  openDeleteQueryChartModal,
53
57
  openDataExportModal,
54
58
  openDeleteDocumentModal,
@@ -92,7 +96,9 @@ import {
92
96
  toggleDataTablesPanel,
93
97
  setCurrentQuery,
94
98
  setChartsHeightPreset,
99
+ setChartsDetailPanelVisibility,
95
100
  setChartsHistoryTab,
101
+ setChartsHistorySearchInput,
96
102
  setChartsHistoryPanelVisibility,
97
103
  setEditorPanelVisibility,
98
104
  setEditorTab,
@@ -103,6 +109,7 @@ import {
103
109
  submitCreateMediaTaggingTagTable,
104
110
  submitCreateMediaTaggingMappingTable,
105
111
  submitDeleteQueryHistoryConfirmation,
112
+ submitDeleteSettingsApiTokenConfirmation,
106
113
  submitRowUpdatePreviewConfirmation,
107
114
  setQueryHistoryPanelVisibility,
108
115
  sortDataTableByColumn,
@@ -113,6 +120,7 @@ import {
113
120
  setCopyColumnModalEditedText,
114
121
  setCopyColumnModalSubmitting,
115
122
  setRoute,
123
+ setSettingsSection,
116
124
  saveQueryHistoryNotes,
117
125
  saveQueryHistoryTitle,
118
126
  saveCurrentTableDesignerDraft,
@@ -217,6 +225,7 @@ let lastRenderedPanelMarkup = '';
217
225
  let lastRenderedModalMarkup = '';
218
226
  let lastRenderedToastMarkup = '';
219
227
  let lastRenderedChartsHistorySignature = '';
228
+ let lastRenderedChartsCardSignature = '';
220
229
  let lastRenderedPanelOpen = false;
221
230
  let lastRenderedLockedRoute = false;
222
231
  let pendingNewTableDesignerAutofocus = false;
@@ -709,7 +718,11 @@ function buildChartsHistorySignature(state) {
709
718
  }
710
719
 
711
720
  return JSON.stringify({
721
+ detailPanelVisible: Boolean(state.charts.detailPanelVisible),
722
+ detailPanelHistoryId: state.charts.detailPanelVisible ? state.charts.selectedHistoryId : null,
712
723
  tab: state.charts.historyTab ?? 'recent',
724
+ searchInput: state.charts.historySearchInput ?? '',
725
+ search: state.charts.historySearch ?? '',
713
726
  queries: queries.map(item => [
714
727
  item.id,
715
728
  item.displayTitle,
@@ -720,6 +733,46 @@ function buildChartsHistorySignature(state) {
720
733
  });
721
734
  }
722
735
 
736
+ const chartSignatureObjectIds = new WeakMap();
737
+ let nextChartSignatureObjectId = 1;
738
+
739
+ function getChartSignatureObjectId(value) {
740
+ if (!value || typeof value !== 'object') {
741
+ return null;
742
+ }
743
+
744
+ if (!chartSignatureObjectIds.has(value)) {
745
+ chartSignatureObjectIds.set(value, nextChartSignatureObjectId);
746
+ nextChartSignatureObjectId += 1;
747
+ }
748
+
749
+ return chartSignatureObjectIds.get(value);
750
+ }
751
+
752
+ function buildChartsCardSignature(state) {
753
+ if (state.route.name !== 'charts' || !state.charts.selectedHistoryId || !state.charts.detail?.item) {
754
+ return '';
755
+ }
756
+
757
+ const charts = state.charts.detail?.charts ?? [];
758
+
759
+ return JSON.stringify({
760
+ selectedHistoryId: state.charts.selectedHistoryId,
761
+ chartHeightPreset: state.charts.chartHeightPreset ?? 'medium',
762
+ resultObjectId: getChartSignatureObjectId(state.charts.result),
763
+ resultLoading: Boolean(state.charts.resultLoading),
764
+ resultError: state.charts.resultError
765
+ ? [state.charts.resultError.code ?? '', state.charts.resultError.message ?? '']
766
+ : null,
767
+ charts: charts.map(chart => [
768
+ chart.id,
769
+ chart.name,
770
+ chart.chartType,
771
+ JSON.stringify(chart.config ?? {}),
772
+ ]),
773
+ });
774
+ }
775
+
723
776
  function syncChartsHistorySelectionUi(state) {
724
777
  const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
725
778
  const historyButtons = shellRefs.view.querySelectorAll(
@@ -733,56 +786,123 @@ function syncChartsHistorySelectionUi(state) {
733
786
 
734
787
  const itemNode = button.closest('[data-charts-history-item]');
735
788
  const isSelected = button.dataset.historyId === selectedHistoryId;
736
- const titleNode = button.querySelector('[data-charts-history-title]');
737
789
  const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
738
790
 
739
- selectionNode.classList.toggle('border-primary-container/30', isSelected);
740
- selectionNode.classList.toggle('bg-surface-container-high', isSelected);
741
- selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
742
- selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
743
- selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
744
-
745
- if (titleNode instanceof HTMLElement) {
746
- titleNode.classList.toggle('text-primary-container', isSelected);
747
- titleNode.classList.toggle('text-on-surface', !isSelected);
748
- }
791
+ selectionNode.classList.toggle('is-active', isSelected);
792
+ button.classList.toggle('is-active', isSelected);
749
793
  }
750
794
 
751
795
  return true;
752
796
  }
753
797
 
754
- function syncChartsSavedToggleUi(actionNode, nextValue) {
755
- const itemNode = actionNode.closest('[data-charts-history-item]');
756
-
757
- actionNode.classList.toggle('is-active', nextValue);
758
- actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
759
- actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
798
+ function syncChartsSavedToggleButtonUi(button, nextValue) {
799
+ button.classList.toggle('is-active', nextValue);
800
+ button.dataset.nextValue = nextValue ? 'false' : 'true';
801
+ button.title = nextValue ? 'Remove from saved' : 'Save query';
760
802
 
761
- const iconNode = actionNode.querySelector('.material-symbols-outlined');
803
+ const iconNode = button.querySelector('.material-symbols-outlined');
762
804
  if (iconNode instanceof HTMLElement) {
763
805
  iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
764
806
  }
765
807
 
766
- if (itemNode instanceof HTMLElement) {
767
- itemNode.querySelector('[data-charts-saved-badge]')?.toggleAttribute('hidden', !nextValue);
808
+ const labelNode = button.querySelector('[data-charts-saved-label]');
809
+ if (labelNode instanceof HTMLElement) {
810
+ labelNode.textContent = nextValue ? 'Unsave' : 'Save';
811
+ }
812
+ }
768
813
 
769
- if (!nextValue && getState().charts.historyTab === 'saved') {
814
+ function syncChartsSavedToggleUi(actionNode, nextValue) {
815
+ const historyId = actionNode.dataset.historyId;
816
+ const relatedButtons = [
817
+ actionNode,
818
+ ...shellRefs.view.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
819
+ ...shellRefs.panel.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
820
+ ].filter((button, index, buttons) => {
821
+ return (
822
+ button instanceof HTMLElement &&
823
+ button.dataset.historyId === historyId &&
824
+ buttons.indexOf(button) === index
825
+ );
826
+ });
827
+
828
+ for (const button of relatedButtons) {
829
+ syncChartsSavedToggleButtonUi(button, nextValue);
830
+
831
+ const itemNode = button.closest('[data-charts-history-item]');
832
+ const historyTab = getState().charts.historyTab;
833
+ if (
834
+ itemNode instanceof HTMLElement &&
835
+ ((historyTab === 'saved' && !nextValue) || (historyTab === 'unsaved' && nextValue))
836
+ ) {
770
837
  itemNode.remove();
771
838
  }
772
839
  }
773
840
 
841
+ for (const badgeNode of [
842
+ ...shellRefs.view.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
843
+ ...shellRefs.panel.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
844
+ ]) {
845
+ if (badgeNode instanceof HTMLElement && badgeNode.dataset.historyId === historyId) {
846
+ badgeNode.toggleAttribute('hidden', !nextValue);
847
+ }
848
+ }
849
+
774
850
  const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
775
851
  if (countNode instanceof HTMLElement) {
776
- const state = getState();
777
- const count =
778
- state.charts.historyTab === 'saved'
779
- ? (state.charts.queries ?? []).filter(item => item.isSaved).length
780
- : (state.charts.queries ?? []).length;
781
- countNode.textContent = String(count);
852
+ countNode.textContent = String(shellRefs.view.querySelectorAll('[data-charts-history-item]').length);
782
853
  }
783
854
  }
784
855
 
785
- function patchChartsDetailUi(state) {
856
+ function renderChartsMainIntoScratch(state) {
857
+ const scratch = document.createElement('div');
858
+
859
+ replaceChildrenFromRenderedMarkup(scratch, renderChartsView(state).main);
860
+ return scratch;
861
+ }
862
+
863
+ function patchChartsHistoryUi(state) {
864
+ const chartsView = shellRefs.view.querySelector('.charts-view');
865
+
866
+ if (!(chartsView instanceof HTMLElement)) {
867
+ return false;
868
+ }
869
+
870
+ const currentSidebar = chartsView.querySelector('.charts-view__sidebar');
871
+ const scratch = renderChartsMainIntoScratch(state);
872
+ const nextSidebar = scratch.querySelector('.charts-view__sidebar');
873
+
874
+ if (!currentSidebar && !nextSidebar) {
875
+ return true;
876
+ }
877
+
878
+ if (!(currentSidebar instanceof HTMLElement) || !(nextSidebar instanceof HTMLElement)) {
879
+ return false;
880
+ }
881
+
882
+ currentSidebar.replaceWith(nextSidebar);
883
+ return true;
884
+ }
885
+
886
+ function renderChartsDetailIntoScratch(state) {
887
+ const scratch = document.createElement('div');
888
+
889
+ replaceChildrenFromRenderedMarkup(scratch, renderChartsDetail(state));
890
+ return scratch;
891
+ }
892
+
893
+ function replaceChartsDetailSection(detailNode, scratchNode, selector) {
894
+ const currentNode = detailNode.querySelector(selector);
895
+ const nextNode = scratchNode.querySelector(selector);
896
+
897
+ if (!(currentNode instanceof HTMLElement) || !(nextNode instanceof HTMLElement)) {
898
+ return false;
899
+ }
900
+
901
+ currentNode.replaceWith(nextNode);
902
+ return true;
903
+ }
904
+
905
+ function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
786
906
  const chartsView = shellRefs.view.querySelector('.charts-view');
787
907
  const detailNode = chartsView?.querySelector('.charts-view__detail');
788
908
 
@@ -797,7 +917,17 @@ function patchChartsDetailUi(state) {
797
917
  return false;
798
918
  }
799
919
 
800
- replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
920
+ if (preserveCharts) {
921
+ const scratch = renderChartsDetailIntoScratch(state);
922
+ const patched = replaceChartsDetailSection(detailNode, scratch, '[data-charts-detail-header]');
923
+
924
+ if (!patched) {
925
+ return false;
926
+ }
927
+ } else {
928
+ replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
929
+ }
930
+
801
931
  syncChartsHistorySelectionUi(state);
802
932
  return true;
803
933
  }
@@ -1223,6 +1353,7 @@ function renderApp(state) {
1223
1353
  const modalMarkup = renderModal(state);
1224
1354
  const toastMarkup = renderToasts(state.toasts);
1225
1355
  const chartsHistorySignature = buildChartsHistorySignature(state);
1356
+ const chartsCardSignature = buildChartsCardSignature(state);
1226
1357
  const isLockedRoute = [
1227
1358
  'editor',
1228
1359
  'editorResults',
@@ -1242,6 +1373,7 @@ function renderApp(state) {
1242
1373
  const panelChanged = panel !== lastRenderedPanelMarkup;
1243
1374
  const modalChanged = modalMarkup !== lastRenderedModalMarkup;
1244
1375
  const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
1376
+ const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
1245
1377
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
1246
1378
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
1247
1379
  const shellMarkupUnchanged =
@@ -1281,12 +1413,25 @@ function renderApp(state) {
1281
1413
  }
1282
1414
 
1283
1415
  const canPatchChartsMain =
1284
- mainChanged && previousRouteName === 'charts' && state.route.name === 'charts' && !chartsHistoryChanged;
1416
+ mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1285
1417
  let mainPatched = false;
1418
+ let preservedChartsDom = false;
1286
1419
 
1287
1420
  if (canPatchChartsMain) {
1288
- teardownQueryChartRenderer();
1289
- mainPatched = patchChartsDetailUi(state);
1421
+ const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
1422
+
1423
+ if (historyPatched) {
1424
+ if (chartsCardsChanged) {
1425
+ teardownQueryChartRenderer();
1426
+ }
1427
+
1428
+ preservedChartsDom = !chartsCardsChanged;
1429
+ mainPatched = patchChartsDetailUi(state, { preserveCharts: preservedChartsDom });
1430
+ }
1431
+
1432
+ if (!mainPatched) {
1433
+ preservedChartsDom = false;
1434
+ }
1290
1435
  }
1291
1436
 
1292
1437
  if (mainChanged) {
@@ -1382,6 +1527,7 @@ function renderApp(state) {
1382
1527
  lastRenderedModalMarkup = modalMarkup;
1383
1528
  lastRenderedToastMarkup = toastMarkup;
1384
1529
  lastRenderedChartsHistorySignature = chartsHistorySignature;
1530
+ lastRenderedChartsCardSignature = chartsCardSignature;
1385
1531
  lastRenderedPanelOpen = panelOpen;
1386
1532
  lastRenderedLockedRoute = isLockedRoute;
1387
1533
 
@@ -1391,7 +1537,7 @@ function renderApp(state) {
1391
1537
  });
1392
1538
  }
1393
1539
 
1394
- if (state.route.name === 'charts') {
1540
+ if (state.route.name === 'charts' && !preservedChartsDom) {
1395
1541
  mountQueryChartRenderer(state);
1396
1542
  }
1397
1543
  }
@@ -2091,6 +2237,35 @@ async function handleAction(actionNode) {
2091
2237
  case 'refresh-view':
2092
2238
  await refreshCurrentRoute();
2093
2239
  return;
2240
+ case 'set-settings-section':
2241
+ setSettingsSection(actionNode.dataset.section);
2242
+ return;
2243
+ case 'check-app-version':
2244
+ await checkSettingsAppVersion();
2245
+ return;
2246
+ case 'open-delete-api-token-modal':
2247
+ openDeleteSettingsApiTokenModal(actionNode.dataset.tokenId);
2248
+ return;
2249
+ case 'copy-created-api-token': {
2250
+ const tokenInput = document.querySelector('[data-created-api-token]');
2251
+ const token = tokenInput instanceof HTMLInputElement ? tokenInput.value : '';
2252
+
2253
+ if (token && navigator.clipboard?.writeText) {
2254
+ await navigator.clipboard.writeText(token);
2255
+ showToast('API token copied.', 'success');
2256
+ }
2257
+ return;
2258
+ }
2259
+ case 'copy-database-id': {
2260
+ const databaseIdNode = document.querySelector('[data-database-id]');
2261
+ const databaseId = databaseIdNode?.textContent?.trim() ?? '';
2262
+
2263
+ if (databaseId && navigator.clipboard?.writeText) {
2264
+ await navigator.clipboard.writeText(databaseId);
2265
+ showToast('Database ID copied.', 'success');
2266
+ }
2267
+ return;
2268
+ }
2094
2269
  case 'open-row-editor-url':
2095
2270
  openRowEditorUrl(actionNode);
2096
2271
  return;
@@ -2181,6 +2356,33 @@ async function handleAction(actionNode) {
2181
2356
  case 'edit-connection':
2182
2357
  openEditConnectionModal(actionNode.dataset.connectionId);
2183
2358
  return;
2359
+ case 'choose-open-database-path': {
2360
+ const labelNode = actionNode.querySelector('[data-open-database-path-button-label]');
2361
+
2362
+ actionNode.setAttribute('disabled', '');
2363
+ if (labelNode) {
2364
+ labelNode.textContent = 'Choosing...';
2365
+ }
2366
+
2367
+ const selectedPath = await chooseOpenDatabasePath();
2368
+ const pathInput = document.querySelector(
2369
+ '[data-form="open-connection"] [data-open-database-path]',
2370
+ );
2371
+
2372
+ if (selectedPath && pathInput instanceof HTMLInputElement) {
2373
+ pathInput.value = selectedPath;
2374
+ pathInput.focus({ preventScroll: true });
2375
+ pathInput.setSelectionRange(pathInput.value.length, pathInput.value.length);
2376
+ }
2377
+
2378
+ if (actionNode.isConnected) {
2379
+ actionNode.removeAttribute('disabled');
2380
+ if (labelNode) {
2381
+ labelNode.textContent = 'Browse...';
2382
+ }
2383
+ }
2384
+ return;
2385
+ }
2184
2386
  case 'choose-create-database-path': {
2185
2387
  const labelNode = actionNode.querySelector('[data-create-database-path-button-label]');
2186
2388
 
@@ -2329,7 +2531,12 @@ async function handleAction(actionNode) {
2329
2531
  await loadMoreQueryHistory();
2330
2532
  return;
2331
2533
  case 'open-query-history':
2332
- if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
2534
+ if (
2535
+ actionNode.dataset.historyId &&
2536
+ openQueryHistoryInEditor(actionNode.dataset.historyId, {
2537
+ notify: getState().route.name !== 'charts',
2538
+ })
2539
+ ) {
2333
2540
  router.navigate('/editor');
2334
2541
  }
2335
2542
  return;
@@ -2362,6 +2569,17 @@ async function handleAction(actionNode) {
2362
2569
  }
2363
2570
  }
2364
2571
  return;
2572
+ case 'open-charts-query-detail':
2573
+ if (actionNode.dataset.historyId) {
2574
+ setChartsDetailPanelVisibility(true);
2575
+ if (getState().route.params?.historyId !== actionNode.dataset.historyId) {
2576
+ router.navigate(`/charts/${encodeURIComponent(actionNode.dataset.historyId)}`);
2577
+ }
2578
+ }
2579
+ return;
2580
+ case 'close-charts-query-detail':
2581
+ setChartsDetailPanelVisibility(false);
2582
+ return;
2365
2583
  case 'toggle-charts-sql-panel':
2366
2584
  toggleChartsSqlPanel();
2367
2585
  return;
@@ -2924,6 +3142,11 @@ document.addEventListener('input', event => {
2924
3142
  return;
2925
3143
  }
2926
3144
 
3145
+ if (bindNode.dataset.bind === 'charts-history-search') {
3146
+ setChartsHistorySearchInput(bindNode.value);
3147
+ return;
3148
+ }
3149
+
2927
3150
  if (bindNode.dataset.bind === 'query-chart-draft:name') {
2928
3151
  updateCurrentQueryChartDraftField('name', bindNode.value);
2929
3152
  }
@@ -3116,6 +3339,9 @@ document.addEventListener('submit', async event => {
3116
3339
  const formData = submitter ? new FormData(form, submitter) : new FormData(form);
3117
3340
 
3118
3341
  switch (form.dataset.form) {
3342
+ case 'create-api-token':
3343
+ await createSettingsApiToken(String(formData.get('name') ?? ''));
3344
+ return;
3119
3345
  case 'open-connection': {
3120
3346
  resetStructureGraphForDatabaseChange();
3121
3347
  const connection = await submitOpenConnection({
@@ -3206,6 +3432,9 @@ document.addEventListener('submit', async event => {
3206
3432
  }
3207
3433
  return;
3208
3434
  }
3435
+ case 'delete-api-token-confirm':
3436
+ await submitDeleteSettingsApiTokenConfirmation();
3437
+ return;
3209
3438
  case 'document-insert-table': {
3210
3439
  const inserted = await submitDocumentInsertTable();
3211
3440
 
@@ -0,0 +1,38 @@
1
+ import { escapeHtml } from '../utils/format.js';
2
+
3
+ export const STANDARD_TEXT_INPUT_CLASS =
4
+ 'control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors placeholder:text-on-surface-variant/35 focus:border-primary-container';
5
+
6
+ export function renderTextInput({
7
+ className = '',
8
+ dataAttributes = {},
9
+ disabled = false,
10
+ maxlength = null,
11
+ name = '',
12
+ placeholder = '',
13
+ readonly = false,
14
+ spellcheck = null,
15
+ type = 'text',
16
+ value = '',
17
+ } = {}) {
18
+ const attributes = [
19
+ `class="${escapeHtml([STANDARD_TEXT_INPUT_CLASS, className].filter(Boolean).join(' '))}"`,
20
+ name ? `name="${escapeHtml(name)}"` : '',
21
+ placeholder ? `placeholder="${escapeHtml(placeholder)}"` : '',
22
+ `type="${escapeHtml(type)}"`,
23
+ `value="${escapeHtml(value)}"`,
24
+ Number.isInteger(maxlength) ? `maxlength="${maxlength}"` : '',
25
+ disabled ? 'disabled' : '',
26
+ readonly ? 'readonly' : '',
27
+ typeof spellcheck === 'boolean' ? `spellcheck="${spellcheck}"` : '',
28
+ ...Object.entries(dataAttributes).map(([name, attributeValue]) => {
29
+ const attributeName = `data-${String(name).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
30
+
31
+ return attributeValue === true
32
+ ? attributeName
33
+ : `${attributeName}="${escapeHtml(attributeValue)}"`;
34
+ }),
35
+ ].filter(Boolean);
36
+
37
+ return `<input ${attributes.join(' ')} />`;
38
+ }