sqlite-hub 0.9.0 → 0.9.3

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.
@@ -69,6 +69,7 @@ import {
69
69
  toggleDataTablesPanel,
70
70
  setCurrentQuery,
71
71
  setChartsHeightPreset,
72
+ setChartsHistoryTab,
72
73
  setEditorPanelVisibility,
73
74
  setEditorTab,
74
75
  submitDeleteChartConfirmation,
@@ -87,6 +88,7 @@ import {
87
88
  toggleChartsResultsPanel,
88
89
  toggleChartsSqlPanel,
89
90
  setMediaTaggingWorkflowMediaDetailsVisible,
91
+ setMediaTaggingWorkflowMediaRotationDegrees,
90
92
  queueTableDesignerCsvImport,
91
93
  showToast,
92
94
  submitCreateConnection,
@@ -110,7 +112,7 @@ import {
110
112
  applyCurrentMediaTaggingSelection,
111
113
  removeCurrentTableDesignerColumn,
112
114
  } from './store.js';
113
- import { renderChartsView } from './views/charts.js';
115
+ import { renderChartsDetail, renderChartsView } from './views/charts.js';
114
116
  import { renderConnectionsView } from './views/connections.js';
115
117
  import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
116
118
  import { renderEditorView } from './views/editor.js';
@@ -137,6 +139,7 @@ const shellRefs = {
137
139
  toast: document.querySelector('#toast-root'),
138
140
  };
139
141
  let lastRenderedRoutePath = null;
142
+ let lastRenderedRouteName = null;
140
143
  let lastRenderedTopNavMarkup = '';
141
144
  let lastRenderedSidebarMarkup = '';
142
145
  let lastRenderedStatusBarMarkup = '';
@@ -144,15 +147,21 @@ let lastRenderedMainMarkup = '';
144
147
  let lastRenderedPanelMarkup = '';
145
148
  let lastRenderedModalMarkup = '';
146
149
  let lastRenderedToastMarkup = '';
150
+ let lastRenderedChartsHistorySignature = '';
147
151
  let lastRenderedPanelOpen = false;
148
152
  let lastRenderedLockedRoute = false;
149
153
  let pendingNewTableDesignerAutofocus = false;
150
154
  let pendingQueryEditorFocus = false;
155
+ let pendingMediaTaggingTagSearchFocus = false;
151
156
 
152
157
  function invalidateMainRenderCache() {
153
158
  lastRenderedMainMarkup = null;
154
159
  }
155
160
 
161
+ function isMediaTaggingRouteName(routeName) {
162
+ return routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue';
163
+ }
164
+
156
165
  function resetStructureGraphForDatabaseChange() {
157
166
  resetPersistedStructureGraphState();
158
167
  }
@@ -258,6 +267,45 @@ function syncMediaTaggingCurrentMediaUi(button, detailsVisible) {
258
267
  return true;
259
268
  }
260
269
 
270
+ function normalizeMediaTaggingRotationDegrees(value) {
271
+ const numericValue = Number(value);
272
+
273
+ if (!Number.isFinite(numericValue)) {
274
+ return 0;
275
+ }
276
+
277
+ return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
278
+ }
279
+
280
+ function syncMediaTaggingMediaRotationUi(node, rotationDegrees) {
281
+ if (!(node instanceof HTMLElement)) {
282
+ return false;
283
+ }
284
+
285
+ const preview = node.closest('.media-tagging-preview');
286
+ const target = preview?.querySelector('[data-media-tagging-rotation-target]');
287
+
288
+ if (!(preview instanceof HTMLElement) || !(target instanceof HTMLElement)) {
289
+ return false;
290
+ }
291
+
292
+ const normalizedRotation = normalizeMediaTaggingRotationDegrees(rotationDegrees);
293
+
294
+ target.style.setProperty('--media-tagging-preview-rotation', `${normalizedRotation}deg`);
295
+ target.dataset.rotationDegrees = String(normalizedRotation);
296
+ target.classList.toggle('is-rotated-quarter', normalizedRotation === 90 || normalizedRotation === 270);
297
+
298
+ const resetButton = preview.querySelector(
299
+ '[data-action="rotate-media-tagging-current-media"][data-rotation-command="reset"]',
300
+ );
301
+
302
+ if (resetButton instanceof HTMLButtonElement) {
303
+ resetButton.disabled = normalizedRotation === 0;
304
+ }
305
+
306
+ return true;
307
+ }
308
+
261
309
  function syncMediaTaggingTagSearchUi(input) {
262
310
  if (!(input instanceof HTMLInputElement)) {
263
311
  return false;
@@ -371,6 +419,188 @@ function syncQueryHistorySelectionUi(selectedHistoryId = null) {
371
419
  return true;
372
420
  }
373
421
 
422
+ function isEditorRouteName(routeName) {
423
+ return routeName === 'editor' || routeName === 'editorResults';
424
+ }
425
+
426
+ function captureQueryHistoryScrollState() {
427
+ if (!isEditorRouteName(lastRenderedRouteName)) {
428
+ return null;
429
+ }
430
+
431
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
432
+
433
+ if (!(scrollNode instanceof HTMLElement)) {
434
+ return null;
435
+ }
436
+
437
+ return {
438
+ committedSearch: scrollNode.dataset.queryHistoryCommittedSearch ?? '',
439
+ historyTab: scrollNode.dataset.queryHistoryTab ?? '',
440
+ routeName: lastRenderedRouteName,
441
+ renderedItemCount: scrollNode.querySelectorAll('.query-history-item').length,
442
+ renderedLoadingMore: scrollNode.dataset.queryHistoryLoadingMore === 'true',
443
+ searchInput: scrollNode.dataset.queryHistorySearch ?? '',
444
+ scrollLeft: scrollNode.scrollLeft,
445
+ scrollTop: scrollNode.scrollTop,
446
+ };
447
+ }
448
+
449
+ function shouldRestoreQueryHistoryScroll(snapshot, state) {
450
+ if (!snapshot || !isEditorRouteName(state.route.name) || snapshot.routeName !== state.route.name) {
451
+ return false;
452
+ }
453
+
454
+ if (!state.editor.historyPanelVisible) {
455
+ return false;
456
+ }
457
+
458
+ if (
459
+ snapshot.historyTab !== state.editor.historyTab ||
460
+ snapshot.searchInput !== state.editor.historySearchInput ||
461
+ snapshot.committedSearch !== state.editor.historySearch
462
+ ) {
463
+ return false;
464
+ }
465
+
466
+ return (
467
+ state.editor.historyLoadingMore ||
468
+ snapshot.renderedLoadingMore ||
469
+ state.editor.history.length > snapshot.renderedItemCount
470
+ );
471
+ }
472
+
473
+ function restoreQueryHistoryScrollState(snapshot) {
474
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
475
+
476
+ if (!(scrollNode instanceof HTMLElement)) {
477
+ return false;
478
+ }
479
+
480
+ scrollNode.scrollLeft = snapshot.scrollLeft;
481
+ scrollNode.scrollTop = snapshot.scrollTop;
482
+ return true;
483
+ }
484
+
485
+ function buildChartsHistorySignature(state) {
486
+ if (state.route.name !== 'charts') {
487
+ return '';
488
+ }
489
+
490
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
491
+
492
+ if (!historyVisible) {
493
+ return 'charts-history:hidden';
494
+ }
495
+
496
+ const queries = state.charts.queries ?? [];
497
+
498
+ if (!queries.length) {
499
+ if (state.charts.loading) {
500
+ return 'charts-history:loading-empty';
501
+ }
502
+
503
+ if (state.charts.error) {
504
+ return `charts-history:error:${state.charts.error.code}:${state.charts.error.message}`;
505
+ }
506
+
507
+ return 'charts-history:empty';
508
+ }
509
+
510
+ return JSON.stringify({
511
+ tab: state.charts.historyTab ?? 'recent',
512
+ queries: queries.map(item => [
513
+ item.id,
514
+ item.displayTitle,
515
+ item.previewSql,
516
+ Boolean(item.isSaved),
517
+ Array.isArray(item.chartTypes) ? item.chartTypes.join(',') : '',
518
+ ]),
519
+ });
520
+ }
521
+
522
+ function syncChartsHistorySelectionUi(state) {
523
+ const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
524
+ const historyButtons = shellRefs.view.querySelectorAll(
525
+ '.charts-view__sidebar [data-action="navigate"][data-history-id]',
526
+ );
527
+
528
+ for (const button of historyButtons) {
529
+ if (!(button instanceof HTMLElement)) {
530
+ continue;
531
+ }
532
+
533
+ const itemNode = button.closest('[data-charts-history-item]');
534
+ const isSelected = button.dataset.historyId === selectedHistoryId;
535
+ const titleNode = button.querySelector('[data-charts-history-title]');
536
+ const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
537
+
538
+ selectionNode.classList.toggle('border-primary-container/30', isSelected);
539
+ selectionNode.classList.toggle('bg-surface-container-high', isSelected);
540
+ selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
541
+ selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
542
+ selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
543
+
544
+ if (titleNode instanceof HTMLElement) {
545
+ titleNode.classList.toggle('text-primary-container', isSelected);
546
+ titleNode.classList.toggle('text-on-surface', !isSelected);
547
+ }
548
+ }
549
+
550
+ return true;
551
+ }
552
+
553
+ function syncChartsSavedToggleUi(actionNode, nextValue) {
554
+ const itemNode = actionNode.closest('[data-charts-history-item]');
555
+
556
+ actionNode.classList.toggle('is-active', nextValue);
557
+ actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
558
+ actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
559
+
560
+ const iconNode = actionNode.querySelector('.material-symbols-outlined');
561
+ if (iconNode instanceof HTMLElement) {
562
+ iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
563
+ }
564
+
565
+ if (itemNode instanceof HTMLElement) {
566
+ itemNode.querySelector('[data-charts-saved-badge]')?.toggleAttribute('hidden', !nextValue);
567
+
568
+ if (!nextValue && getState().charts.historyTab === 'saved') {
569
+ itemNode.remove();
570
+ }
571
+ }
572
+
573
+ const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
574
+ if (countNode instanceof HTMLElement) {
575
+ const state = getState();
576
+ const count =
577
+ state.charts.historyTab === 'saved'
578
+ ? (state.charts.queries ?? []).filter(item => item.isSaved).length
579
+ : (state.charts.queries ?? []).length;
580
+ countNode.textContent = String(count);
581
+ }
582
+ }
583
+
584
+ function patchChartsDetailUi(state) {
585
+ const chartsView = shellRefs.view.querySelector('.charts-view');
586
+ const detailNode = chartsView?.querySelector('.charts-view__detail');
587
+
588
+ if (!(chartsView instanceof HTMLElement) || !(detailNode instanceof HTMLElement)) {
589
+ return false;
590
+ }
591
+
592
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
593
+ const sidebarNode = chartsView.querySelector('.charts-view__sidebar');
594
+
595
+ if (Boolean(sidebarNode) !== historyVisible) {
596
+ return false;
597
+ }
598
+
599
+ detailNode.innerHTML = renderChartsDetail(state);
600
+ syncChartsHistorySelectionUi(state);
601
+ return true;
602
+ }
603
+
374
604
  function readFileAsBase64(file) {
375
605
  return new Promise((resolve, reject) => {
376
606
  const reader = new FileReader();
@@ -615,6 +845,52 @@ function focusQueryEditorInput() {
615
845
  return true;
616
846
  }
617
847
 
848
+ function focusMediaTaggingTagSearchInput() {
849
+ const input = document.querySelector('[data-bind="media-tagging-tag-search"]');
850
+
851
+ if (!(input instanceof HTMLInputElement)) {
852
+ return false;
853
+ }
854
+
855
+ input.focus({ preventScroll: true });
856
+ input.setSelectionRange(input.value.length, input.value.length);
857
+ return true;
858
+ }
859
+
860
+ function syncSidebarActiveRoute(routeName) {
861
+ if (!isMediaTaggingRouteName(routeName)) {
862
+ return false;
863
+ }
864
+
865
+ const mediaTaggingLink = shellRefs.sidebar.querySelector('a.sidebar-link[data-group="mediaTagging"]');
866
+ const setupLink = shellRefs.sidebar.querySelector('a.sidebar-sublink[href="#/media-tagging"]');
867
+ const queueLink = shellRefs.sidebar.querySelector('a.sidebar-sublink[href="#/media-tagging/queue"]');
868
+
869
+ if (
870
+ !(mediaTaggingLink instanceof HTMLAnchorElement) ||
871
+ !(setupLink instanceof HTMLAnchorElement) ||
872
+ !(queueLink instanceof HTMLAnchorElement)
873
+ ) {
874
+ return false;
875
+ }
876
+
877
+ mediaTaggingLink.classList.add('is-active');
878
+ setupLink.classList.toggle('is-active', routeName === 'mediaTaggingSetup');
879
+ queueLink.classList.toggle('is-active', routeName === 'mediaTaggingQueue');
880
+ return true;
881
+ }
882
+
883
+ async function applyMediaTaggingAndFocusSearch() {
884
+ pendingMediaTaggingTagSearchFocus = true;
885
+ const result = await applyCurrentMediaTaggingSelection();
886
+
887
+ if (!result) {
888
+ pendingMediaTaggingTagSearchFocus = false;
889
+ }
890
+
891
+ return result;
892
+ }
893
+
618
894
  async function handleTableDesignerCsvImport(fileInput) {
619
895
  if (!(fileInput instanceof HTMLInputElement)) {
620
896
  return;
@@ -656,12 +932,14 @@ async function handleTableDesignerCsvImport(fileInput) {
656
932
 
657
933
  function renderApp(state) {
658
934
  const previousRoutePath = lastRenderedRoutePath;
935
+ const previousRouteName = lastRenderedRouteName;
659
936
  const { main, panel } = resolveView(state);
660
937
  const topNavMarkup = renderTopNav(state);
661
938
  const sidebarMarkup = renderSidebar(state);
662
939
  const statusBarMarkup = renderStatusBar(state);
663
940
  const modalMarkup = renderModal(state);
664
941
  const toastMarkup = renderToasts(state.toasts);
942
+ const chartsHistorySignature = buildChartsHistorySignature(state);
665
943
  const isLockedRoute = [
666
944
  'editor',
667
945
  'editorResults',
@@ -679,6 +957,7 @@ function renderApp(state) {
679
957
  const mainChanged = main !== lastRenderedMainMarkup;
680
958
  const panelChanged = panel !== lastRenderedPanelMarkup;
681
959
  const modalChanged = modalMarkup !== lastRenderedModalMarkup;
960
+ const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
682
961
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
683
962
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
684
963
  const shellMarkupUnchanged =
@@ -703,6 +982,7 @@ function renderApp(state) {
703
982
  }
704
983
 
705
984
  const focusedInput = captureFocusedInputState();
985
+ const queryHistoryScrollState = captureQueryHistoryScrollState();
706
986
  const isEnteringNewTableDesignerRoute =
707
987
  state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
708
988
 
@@ -712,9 +992,20 @@ function renderApp(state) {
712
992
  pendingNewTableDesignerAutofocus = false;
713
993
  }
714
994
 
715
- if (mainChanged) {
716
- teardownStructureGraph();
995
+ const canPatchChartsMain =
996
+ mainChanged && previousRouteName === 'charts' && state.route.name === 'charts' && !chartsHistoryChanged;
997
+ let mainPatched = false;
998
+
999
+ if (canPatchChartsMain) {
717
1000
  teardownQueryChartRenderer();
1001
+ mainPatched = patchChartsDetailUi(state);
1002
+ }
1003
+
1004
+ if (mainChanged) {
1005
+ if (!mainPatched) {
1006
+ teardownStructureGraph();
1007
+ teardownQueryChartRenderer();
1008
+ }
718
1009
  }
719
1010
 
720
1011
  if (topNavChanged) {
@@ -722,14 +1013,21 @@ function renderApp(state) {
722
1013
  }
723
1014
 
724
1015
  if (sidebarChanged) {
725
- shellRefs.sidebar.innerHTML = sidebarMarkup;
1016
+ const sidebarSynced =
1017
+ isMediaTaggingRouteName(previousRouteName) &&
1018
+ isMediaTaggingRouteName(state.route.name) &&
1019
+ syncSidebarActiveRoute(state.route.name);
1020
+
1021
+ if (!sidebarSynced) {
1022
+ shellRefs.sidebar.innerHTML = sidebarMarkup;
1023
+ }
726
1024
  }
727
1025
 
728
1026
  if (statusBarChanged) {
729
1027
  shellRefs.statusBar.innerHTML = statusBarMarkup;
730
1028
  }
731
1029
 
732
- if (mainChanged) {
1030
+ if (mainChanged && !mainPatched) {
733
1031
  shellRefs.view.innerHTML = main;
734
1032
  }
735
1033
 
@@ -753,10 +1051,18 @@ function renderApp(state) {
753
1051
  shellRefs.shell.classList.toggle('panel-open', panelOpen);
754
1052
  }
755
1053
 
1054
+ if (shouldRestoreQueryHistoryScroll(queryHistoryScrollState, state)) {
1055
+ restoreQueryHistoryScrollState(queryHistoryScrollState);
1056
+ }
1057
+
756
1058
  if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
757
1059
  if (focusQueryEditorInput()) {
758
1060
  pendingQueryEditorFocus = false;
759
1061
  }
1062
+ } else if (pendingMediaTaggingTagSearchFocus && state.route.name === 'mediaTaggingQueue') {
1063
+ if (focusMediaTaggingTagSearchInput()) {
1064
+ pendingMediaTaggingTagSearchFocus = false;
1065
+ }
760
1066
  } else if (
761
1067
  pendingNewTableDesignerAutofocus &&
762
1068
  state.route.name === 'tableDesigner' &&
@@ -771,6 +1077,7 @@ function renderApp(state) {
771
1077
  }
772
1078
 
773
1079
  lastRenderedRoutePath = state.route.path;
1080
+ lastRenderedRouteName = state.route.name;
774
1081
  lastRenderedTopNavMarkup = topNavMarkup;
775
1082
  lastRenderedSidebarMarkup = sidebarMarkup;
776
1083
  lastRenderedStatusBarMarkup = statusBarMarkup;
@@ -778,6 +1085,7 @@ function renderApp(state) {
778
1085
  lastRenderedPanelMarkup = panel;
779
1086
  lastRenderedModalMarkup = modalMarkup;
780
1087
  lastRenderedToastMarkup = toastMarkup;
1088
+ lastRenderedChartsHistorySignature = chartsHistorySignature;
781
1089
  lastRenderedPanelOpen = panelOpen;
782
1090
  lastRenderedLockedRoute = isLockedRoute;
783
1091
 
@@ -958,6 +1266,20 @@ async function handleAction(actionNode) {
958
1266
  );
959
1267
  }
960
1268
  return;
1269
+ case 'toggle-charts-query-history-saved':
1270
+ if (actionNode.dataset.historyId) {
1271
+ const nextValue = actionNode.dataset.nextValue === 'true';
1272
+ const updated = await toggleQueryHistorySavedState(actionNode.dataset.historyId, nextValue, {
1273
+ notify: false,
1274
+ refresh: false,
1275
+ toast: false,
1276
+ });
1277
+
1278
+ if (updated) {
1279
+ syncChartsSavedToggleUi(actionNode, nextValue);
1280
+ }
1281
+ }
1282
+ return;
961
1283
  case 'toggle-charts-sql-panel':
962
1284
  toggleChartsSqlPanel();
963
1285
  return;
@@ -969,6 +1291,11 @@ async function handleAction(actionNode) {
969
1291
  setChartsHeightPreset(actionNode.dataset.preset);
970
1292
  }
971
1293
  return;
1294
+ case 'set-charts-history-tab':
1295
+ if (actionNode.dataset.tab) {
1296
+ setChartsHistoryTab(actionNode.dataset.tab);
1297
+ }
1298
+ return;
972
1299
  case 'export-query-chart-png':
973
1300
  if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
974
1301
  showToast('The selected chart is not ready for PNG export.', 'alert');
@@ -1071,7 +1398,7 @@ async function handleAction(actionNode) {
1071
1398
  await resetSkippedMediaTaggingItems();
1072
1399
  return;
1073
1400
  case 'apply-media-tagging':
1074
- await applyCurrentMediaTaggingSelection();
1401
+ await applyMediaTaggingAndFocusSearch();
1075
1402
  return;
1076
1403
  case 'toggle-media-tagging-current-media':
1077
1404
  if (actionNode instanceof HTMLButtonElement) {
@@ -1080,6 +1407,20 @@ async function handleAction(actionNode) {
1080
1407
  setMediaTaggingWorkflowMediaDetailsVisible(nextValue, { notify: false });
1081
1408
  }
1082
1409
  return;
1410
+ case 'rotate-media-tagging-current-media': {
1411
+ const currentRotation = getState().mediaTagging.workflowMediaRotationDegrees ?? 0;
1412
+ const command = actionNode.dataset.rotationCommand;
1413
+ const nextRotation =
1414
+ command === 'left'
1415
+ ? currentRotation - 90
1416
+ : command === 'right'
1417
+ ? currentRotation + 90
1418
+ : 0;
1419
+
1420
+ syncMediaTaggingMediaRotationUi(actionNode, nextRotation);
1421
+ setMediaTaggingWorkflowMediaRotationDegrees(nextRotation, { notify: false });
1422
+ return;
1423
+ }
1083
1424
  case 'open-media-tagging-current-in-data': {
1084
1425
  const currentState = getState();
1085
1426
  const mediaTableName = currentState.mediaTagging.draft?.mediaTable ?? '';
@@ -1093,6 +1434,17 @@ async function handleAction(actionNode) {
1093
1434
  router.navigate(`/data/${encodeURIComponent(mediaTableName)}`);
1094
1435
  return;
1095
1436
  }
1437
+ case 'open-media-tagging-current-in-structure': {
1438
+ const mediaTableName = String(getState().mediaTagging.draft?.mediaTable ?? '').trim();
1439
+
1440
+ if (!mediaTableName) {
1441
+ showToast('The current media table could not be opened in Structure.', 'alert');
1442
+ return;
1443
+ }
1444
+
1445
+ router.navigate(`/structure/${encodeURIComponent(mediaTableName)}`);
1446
+ return;
1447
+ }
1096
1448
  case 'import-table-designer-csv': {
1097
1449
  const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
1098
1450
 
@@ -1149,6 +1501,19 @@ async function handleAction(actionNode) {
1149
1501
  }
1150
1502
  }
1151
1503
 
1504
+ function isMediaTaggingMenuActive(routeName) {
1505
+ return routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue';
1506
+ }
1507
+
1508
+ function canApplyMediaTaggingShortcut(state) {
1509
+ return (
1510
+ isMediaTaggingMenuActive(state.route.name) &&
1511
+ Boolean(state.mediaTagging.workflow?.currentItem) &&
1512
+ !state.mediaTagging.applying &&
1513
+ (state.mediaTagging.selectedTagKeys?.length ?? 0) > 0
1514
+ );
1515
+ }
1516
+
1152
1517
  document.addEventListener('click', event => {
1153
1518
  const actionNode = event.target.closest('[data-action]');
1154
1519
 
@@ -1161,6 +1526,7 @@ document.addEventListener('click', event => {
1161
1526
 
1162
1527
  document.addEventListener('keydown', event => {
1163
1528
  const target = event.target;
1529
+ const state = getState();
1164
1530
 
1165
1531
  // Handle Enter key in tag form fields to trigger create tag
1166
1532
  if (
@@ -1181,6 +1547,20 @@ document.addEventListener('keydown', event => {
1181
1547
  return;
1182
1548
  }
1183
1549
 
1550
+ if (
1551
+ (event.key === 'Enter' || event.code === 'Enter' || event.code === 'NumpadEnter') &&
1552
+ event.shiftKey &&
1553
+ !event.altKey &&
1554
+ !event.ctrlKey &&
1555
+ !event.metaKey &&
1556
+ !event.defaultPrevented &&
1557
+ canApplyMediaTaggingShortcut(state)
1558
+ ) {
1559
+ event.preventDefault();
1560
+ void applyMediaTaggingAndFocusSearch();
1561
+ return;
1562
+ }
1563
+
1184
1564
  if (
1185
1565
  event.key === 'Enter' &&
1186
1566
  event.shiftKey &&
@@ -1204,8 +1584,6 @@ document.addEventListener('keydown', event => {
1204
1584
  return;
1205
1585
  }
1206
1586
 
1207
- const state = getState();
1208
-
1209
1587
  if (state.modal) {
1210
1588
  event.preventDefault();
1211
1589
  closeModal();
@@ -481,13 +481,22 @@ function renderChartEditorForm(modal, state) {
481
481
  bind: "query-chart-draft-config:y_column",
482
482
  })}
483
483
  </div>
484
- <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
484
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
485
+ ${renderSelectField({
486
+ label: "Sort By",
487
+ value: draft.config?.sort_by ?? "y",
488
+ options: [
489
+ { value: "x", label: "X column" },
490
+ { value: "y", label: "Y value" },
491
+ ],
492
+ bind: "query-chart-draft-config:sort_by",
493
+ })}
485
494
  ${renderSelectField({
486
495
  label: "Sort Direction",
487
- value: draft.config?.sort_direction ?? "asc",
496
+ value: draft.config?.sort_direction ?? "desc",
488
497
  options: [
489
- { value: "asc", label: "Ascending" },
490
- { value: "desc", label: "Descending" },
498
+ { value: "asc", label: "Ascending / smallest first" },
499
+ { value: "desc", label: "Descending / largest first" },
491
500
  ],
492
501
  bind: "query-chart-draft-config:sort_direction",
493
502
  })}
@@ -23,6 +23,18 @@ function canOpenQueryHistoryInCharts(item) {
23
23
  return Boolean(item?.chartsEligible);
24
24
  }
25
25
 
26
+ function getQueryTypeTone(queryType) {
27
+ if (queryType === "select" || queryType === "update") {
28
+ return "success";
29
+ }
30
+
31
+ if (queryType === "pragma") {
32
+ return "primary";
33
+ }
34
+
35
+ return "muted";
36
+ }
37
+
26
38
  function renderRunItem(run) {
27
39
  return `
28
40
  <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
@@ -135,9 +147,9 @@ export function renderQueryHistoryDetail({
135
147
  </button>
136
148
  </div>
137
149
  <div class="mt-4 flex flex-wrap gap-2">
138
- ${renderStatusBadge(item.queryType, item.isDestructive ? "alert" : "primary")}
150
+ ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
139
151
  ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
140
- ${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
152
+ ${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
141
153
  ${item.lastRun ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success") : ""}
142
154
  </div>
143
155
  </div>
@@ -4,12 +4,8 @@ import {
4
4
  } from "../utils/format.js";
5
5
  import { renderStatusBadge } from "./badges.js";
6
6
 
7
- function getQueryTypeTone(queryType, isDestructive) {
8
- if (isDestructive) {
9
- return "alert";
10
- }
11
-
12
- if (queryType === "select") {
7
+ function getQueryTypeTone(queryType) {
8
+ if (queryType === "select" || queryType === "update") {
13
9
  return "success";
14
10
  }
15
11
 
@@ -39,9 +35,9 @@ export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistor
39
35
  <span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
40
36
  ${escapeHtml(item.displayTitle)}
41
37
  </span>
42
- ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType, item.isDestructive))}
38
+ ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType))}
43
39
  ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
44
- ${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
40
+ ${item.isDestructive ? renderStatusBadge("destructive", "warning") : ""}
45
41
  </div>
46
42
  <p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
47
43
  ${escapeHtml(item.previewSql)}
@@ -143,6 +139,7 @@ export function renderQueryHistoryPanel({
143
139
  error = null,
144
140
  activeTab = "recent",
145
141
  search = "",
142
+ committedSearch = "",
146
143
  total = 0,
147
144
  hasMore = false,
148
145
  activeHistoryId = null,
@@ -180,7 +177,14 @@ export function renderQueryHistoryPanel({
180
177
  />
181
178
  </label>
182
179
  </div>
183
- <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3">
180
+ <div
181
+ class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3"
182
+ data-query-history-committed-search="${escapeHtml(committedSearch)}"
183
+ data-query-history-loading-more="${loadingMore ? "true" : "false"}"
184
+ data-query-history-search="${escapeHtml(search)}"
185
+ data-query-history-scroll
186
+ data-query-history-tab="${escapeHtml(activeTab)}"
187
+ >
184
188
  ${
185
189
  error
186
190
  ? `