sqlite-hub 0.7.0 → 0.9.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 (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
@@ -1,211 +1,423 @@
1
- import { renderAppShell } from "./components/appShell.js";
2
- import { renderModal } from "./components/modal.js";
3
- import { renderSidebar } from "./components/sidebar.js";
4
- import { renderStatusBar } from "./components/statusBar.js";
1
+ import { renderAppShell } from './components/appShell.js';
2
+ import { renderModal } from './components/modal.js';
3
+ import { renderQueryHistoryDetail } from './components/queryHistoryDetail.js';
4
+ import { renderQueryHistoryListItem } from './components/queryHistoryPanel.js';
5
+ import { renderSidebar } from './components/sidebar.js';
6
+ import { renderStatusBar } from './components/statusBar.js';
5
7
  import {
6
- mountStructureGraph,
7
- teardownStructureGraph,
8
- resetPersistedStructureGraphState,
9
- } from "./components/structureGraph.js";
8
+ mountStructureGraph,
9
+ teardownStructureGraph,
10
+ resetPersistedStructureGraphState,
11
+ } from './components/structureGraph.js';
10
12
  import {
11
- exportQueryChartAsPng,
12
- mountQueryChartRenderer,
13
- teardownQueryChartRenderer,
14
- } from "./components/queryChartRenderer.js";
15
- import { renderToasts } from "./components/toast.js";
16
- import { renderTopNav } from "./components/topNav.js";
17
- import { createRouter } from "./router.js";
13
+ exportQueryChartAsPng,
14
+ mountQueryChartRenderer,
15
+ teardownQueryChartRenderer,
16
+ } from './components/queryChartRenderer.js';
17
+ import { renderToasts } from './components/toast.js';
18
+ import { renderTopNav } from './components/topNav.js';
19
+ import { createRouter } from './router.js';
18
20
  import {
19
- createActiveConnectionBackup,
20
- clearCurrentQuery,
21
- clearDataRowSelection,
22
- clearEditorRowSelection,
23
- clearEditorResults,
24
- clearQueryHistorySelection,
25
- closeModal,
26
- deleteQueryHistoryStateItem,
27
- dismissToast,
28
- executeCurrentQuery,
29
- exportCurrentDataTableCsv,
30
- exportCurrentQueryCsv,
31
- getState,
32
- initializeApp,
33
- loadMoreQueryHistory,
34
- openModal,
35
- openOverviewInFinder,
36
- openQueryHistoryInEditor,
37
- openDeleteDataRowModal,
38
- openDeleteEditorRowModal,
39
- openDeleteQueryChartModal,
40
- openEditConnectionModal,
41
- openCreateQueryChartModal,
42
- openEditQueryChartModal,
43
- refreshCurrentRoute,
44
- removeConnection,
45
- runQueryHistoryItem,
46
- saveCurrentQueryChartDraft,
47
- selectDataRow,
48
- selectEditorRow,
49
- selectConnection,
50
- selectQueryHistoryItem,
51
- selectStructureEntry,
52
- setTableDesignerSearchQuery,
53
- setDataPage,
54
- setDataPageSize,
55
- setDataSearchColumn,
56
- setDataSearchQuery,
57
- toggleDataTablesPanel,
58
- setCurrentQuery,
59
- setChartsHeightPreset,
60
- setEditorPanelVisibility,
61
- setEditorTab,
62
- submitDeleteChartConfirmation,
63
- setQueryHistoryPanelVisibility,
64
- sortDataTableByColumn,
65
- sortEditorResultsByColumn,
66
- setQueryHistorySearchInput,
67
- setQueryHistoryTab,
68
- setRoute,
69
- saveQueryHistoryNotes,
70
- saveQueryHistoryTitle,
71
- saveCurrentTableDesignerDraft,
72
- toggleChartsResultsPanel,
73
- toggleChartsSqlPanel,
74
- queueTableDesignerCsvImport,
75
- showToast,
76
- submitCreateConnection,
77
- submitDeleteRowConfirmation,
78
- submitDataRowUpdate,
79
- submitEditorRowUpdate,
80
- submitEditConnection,
81
- submitImportSql,
82
- submitOpenConnection,
83
- subscribe,
84
- toggleQueryHistorySavedState,
85
- updateCurrentQueryChartDraftConfigField,
86
- updateCurrentQueryChartDraftField,
87
- updateCurrentTableDesignerColumnField,
88
- updateCurrentTableDesignerField,
89
- addCurrentTableDesignerColumn,
90
- removeCurrentTableDesignerColumn,
91
- } from "./store.js";
92
- import { renderChartsView } from "./views/charts.js";
93
- import { renderConnectionsView } from "./views/connections.js";
94
- import { renderDataView } from "./views/data.js";
95
- import { renderEditorView } from "./views/editor.js";
96
- import { renderLandingView } from "./views/landing.js";
97
- import { renderOverviewView } from "./views/overview.js";
98
- import { renderSettingsView } from "./views/settings.js";
99
- import { renderStructureView } from "./views/structure.js";
100
- import { renderTableDesignerView } from "./views/tableDesigner.js";
101
- import { highlightSql } from "./utils/format.js";
102
-
103
- const appRoot = document.querySelector("#app");
21
+ createActiveConnectionBackup,
22
+ clearCurrentQuery,
23
+ clearDataRowSelection,
24
+ clearEditorRowSelection,
25
+ clearEditorResults,
26
+ clearQueryHistorySelection,
27
+ closeModal,
28
+ dismissMediaTaggingIssue,
29
+ dismissToast,
30
+ executeCurrentQuery,
31
+ exportCurrentDataTableCsv,
32
+ exportCurrentQueryCsv,
33
+ getState,
34
+ initializeApp,
35
+ loadMoreQueryHistory,
36
+ openModal,
37
+ openOverviewInFinder,
38
+ openQueryHistoryInEditor,
39
+ openDeleteDataRowModal,
40
+ openDeleteEditorRowModal,
41
+ openDeleteQueryHistoryModal,
42
+ openDeleteQueryChartModal,
43
+ openDataRowByIdentity,
44
+ openEditConnectionModal,
45
+ openCreateQueryChartModal,
46
+ openEditQueryChartModal,
47
+ refreshCurrentRoute,
48
+ refreshMediaTaggingPreview,
49
+ removeConnection,
50
+ removeCurrentMediaTag,
51
+ resetMediaTaggingQueriesToDefault,
52
+ resetSkippedMediaTaggingItems,
53
+ runQueryHistoryItem,
54
+ skipCurrentMediaTaggingItem,
55
+ saveCurrentQueryChartDraft,
56
+ saveCurrentMediaTaggingConfig,
57
+ selectDataRow,
58
+ selectEditorRow,
59
+ selectConnection,
60
+ selectQueryHistoryItem,
61
+ selectStructureEntry,
62
+ setTableDesignerSearchQuery,
63
+ setTableDesignerSqlPreviewVisibility,
64
+ toggleStructureTablesPanel,
65
+ setDataPage,
66
+ setDataPageSize,
67
+ setDataSearchColumn,
68
+ setDataSearchQuery,
69
+ toggleDataTablesPanel,
70
+ setCurrentQuery,
71
+ setChartsHeightPreset,
72
+ setEditorPanelVisibility,
73
+ setEditorTab,
74
+ submitDeleteChartConfirmation,
75
+ submitCreateMediaTaggingTagTable,
76
+ submitCreateMediaTaggingMappingTable,
77
+ submitDeleteQueryHistoryConfirmation,
78
+ setQueryHistoryPanelVisibility,
79
+ sortDataTableByColumn,
80
+ sortEditorResultsByColumn,
81
+ setQueryHistorySearchInput,
82
+ setQueryHistoryTab,
83
+ setRoute,
84
+ saveQueryHistoryNotes,
85
+ saveQueryHistoryTitle,
86
+ saveCurrentTableDesignerDraft,
87
+ toggleChartsResultsPanel,
88
+ toggleChartsSqlPanel,
89
+ setMediaTaggingWorkflowMediaDetailsVisible,
90
+ queueTableDesignerCsvImport,
91
+ showToast,
92
+ submitCreateConnection,
93
+ createCurrentMediaTag,
94
+ submitDeleteRowConfirmation,
95
+ submitDataRowUpdate,
96
+ submitEditorRowUpdate,
97
+ submitEditConnection,
98
+ submitImportSql,
99
+ submitOpenConnection,
100
+ subscribe,
101
+ toggleCurrentMediaTagSelection,
102
+ toggleQueryHistorySavedState,
103
+ updateCurrentMediaTaggingField,
104
+ updateCurrentMediaTaggingTagFormField,
105
+ updateCurrentQueryChartDraftConfigField,
106
+ updateCurrentQueryChartDraftField,
107
+ updateCurrentTableDesignerColumnField,
108
+ updateCurrentTableDesignerField,
109
+ addCurrentTableDesignerColumn,
110
+ applyCurrentMediaTaggingSelection,
111
+ removeCurrentTableDesignerColumn,
112
+ } from './store.js';
113
+ import { renderChartsView } from './views/charts.js';
114
+ import { renderConnectionsView } from './views/connections.js';
115
+ import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
116
+ import { renderEditorView } from './views/editor.js';
117
+ import { renderLandingView } from './views/landing.js';
118
+ import { renderMediaTaggingView } from './views/mediaTagging.js';
119
+ import { renderOverviewView } from './views/overview.js';
120
+ import { renderSettingsView } from './views/settings.js';
121
+ import { renderStructureView } from './views/structure.js';
122
+ import { renderTableDesignerView } from './views/tableDesigner.js';
123
+ import { highlightSql } from './utils/format.js';
124
+
125
+ const appRoot = document.querySelector('#app');
104
126
 
105
127
  appRoot.innerHTML = renderAppShell();
106
128
 
107
129
  const shellRefs = {
108
- shell: document.querySelector(".app-shell"),
109
- topNav: document.querySelector("#top-nav"),
110
- sidebar: document.querySelector("#sidebar"),
111
- view: document.querySelector("#app-view"),
112
- panel: document.querySelector("#app-panel"),
113
- statusBar: document.querySelector("#status-bar"),
114
- modal: document.querySelector("#modal-root"),
115
- toast: document.querySelector("#toast-root"),
130
+ shell: document.querySelector('.app-shell'),
131
+ topNav: document.querySelector('#top-nav'),
132
+ sidebar: document.querySelector('#sidebar'),
133
+ view: document.querySelector('#app-view'),
134
+ panel: document.querySelector('#app-panel'),
135
+ statusBar: document.querySelector('#status-bar'),
136
+ modal: document.querySelector('#modal-root'),
137
+ toast: document.querySelector('#toast-root'),
116
138
  };
117
139
  let lastRenderedRoutePath = null;
140
+ let lastRenderedTopNavMarkup = '';
141
+ let lastRenderedSidebarMarkup = '';
142
+ let lastRenderedStatusBarMarkup = '';
143
+ let lastRenderedMainMarkup = '';
144
+ let lastRenderedPanelMarkup = '';
145
+ let lastRenderedModalMarkup = '';
146
+ let lastRenderedToastMarkup = '';
147
+ let lastRenderedPanelOpen = false;
148
+ let lastRenderedLockedRoute = false;
118
149
  let pendingNewTableDesignerAutofocus = false;
150
+ let pendingQueryEditorFocus = false;
151
+
152
+ function invalidateMainRenderCache() {
153
+ lastRenderedMainMarkup = null;
154
+ }
119
155
 
120
156
  function resetStructureGraphForDatabaseChange() {
121
- resetPersistedStructureGraphState();
157
+ resetPersistedStructureGraphState();
122
158
  }
123
159
 
124
160
  function renderQueryHighlightMarkup(query) {
125
- if (query) {
126
- return highlightSql(query);
127
- }
161
+ if (query) {
162
+ return highlightSql(query);
163
+ }
128
164
 
129
- return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
165
+ return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
130
166
  }
131
167
 
132
168
  function syncQueryEditorHighlight(textarea) {
133
- if (!(textarea instanceof HTMLTextAreaElement)) {
134
- return;
135
- }
169
+ if (!(textarea instanceof HTMLTextAreaElement)) {
170
+ return;
171
+ }
136
172
 
137
- const layer = textarea.closest(".query-editor-layer");
138
- const highlightNode = layer?.querySelector("[data-query-editor-highlight]");
173
+ const layer = textarea.closest('.query-editor-layer');
174
+ const highlightNode = layer?.querySelector('[data-query-editor-highlight]');
139
175
 
140
- if (!(highlightNode instanceof HTMLElement)) {
141
- return;
142
- }
176
+ if (!(highlightNode instanceof HTMLElement)) {
177
+ return;
178
+ }
143
179
 
144
- highlightNode.innerHTML = renderQueryHighlightMarkup(textarea.value);
180
+ highlightNode.innerHTML = renderQueryHighlightMarkup(textarea.value);
145
181
  }
146
182
 
147
183
  function syncQueryEditorScroll(textarea) {
148
- if (!(textarea instanceof HTMLTextAreaElement)) {
149
- return;
150
- }
184
+ if (!(textarea instanceof HTMLTextAreaElement)) {
185
+ return;
186
+ }
151
187
 
152
- const layer = textarea.closest(".query-editor-layer");
153
- const highlightNode = layer?.querySelector("[data-query-editor-highlight]");
188
+ const layer = textarea.closest('.query-editor-layer');
189
+ const highlightNode = layer?.querySelector('[data-query-editor-highlight]');
190
+ const gutterNode = textarea.closest('.query-editor-shell')?.querySelector('[data-query-editor-gutter]');
154
191
 
155
- if (!(highlightNode instanceof HTMLElement)) {
156
- return;
157
- }
192
+ if (!(highlightNode instanceof HTMLElement)) {
193
+ return;
194
+ }
195
+
196
+ highlightNode.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`;
158
197
 
159
- highlightNode.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`;
198
+ if (gutterNode instanceof HTMLElement) {
199
+ gutterNode.style.transform = `translateY(${-textarea.scrollTop}px)`;
200
+ }
160
201
  }
161
202
 
162
- function readFileAsBase64(file) {
163
- return new Promise((resolve, reject) => {
164
- const reader = new FileReader();
203
+ function syncMediaTagSelectionUi(input, checked) {
204
+ if (!(input instanceof HTMLInputElement)) {
205
+ return;
206
+ }
165
207
 
166
- reader.onload = () => {
167
- const result = String(reader.result ?? "");
168
- const markerIndex = result.indexOf(",");
169
- resolve(markerIndex >= 0 ? result.slice(markerIndex + 1) : result);
170
- };
171
- reader.onerror = () => {
172
- reject(reader.error ?? new Error("The selected logo could not be read."));
173
- };
208
+ const tagOption = input.closest('.media-tagging-tag-option');
209
+ input.checked = Boolean(checked);
210
+ tagOption?.classList.toggle('is-selected', Boolean(checked));
211
+ syncMediaTaggingApplyButtonUi(input);
212
+ }
213
+
214
+ function syncMediaTaggingApplyButtonUi(node) {
215
+ if (!(node instanceof HTMLElement)) {
216
+ return false;
217
+ }
218
+
219
+ const tagPanel = node.closest('.media-tagging-tag-panel');
220
+
221
+ if (!(tagPanel instanceof HTMLElement)) {
222
+ return false;
223
+ }
224
+
225
+ const applyButton = tagPanel.querySelector('[data-action="apply-media-tagging"]');
226
+
227
+ if (!(applyButton instanceof HTMLButtonElement)) {
228
+ return false;
229
+ }
230
+
231
+ const selectedCount = tagPanel.querySelectorAll('.media-tagging-tag-option__checkbox:checked').length;
232
+ const canApply = applyButton.dataset.canApply === 'true';
233
+ applyButton.textContent = `${selectedCount} tagged & next`;
234
+ applyButton.disabled = !canApply || selectedCount < 1;
235
+ return true;
236
+ }
237
+
238
+ function syncMediaTaggingCurrentMediaUi(button, detailsVisible) {
239
+ if (!(button instanceof HTMLButtonElement)) {
240
+ return false;
241
+ }
242
+
243
+ const preview = button.closest('.media-tagging-preview');
244
+
245
+ if (!(preview instanceof HTMLElement)) {
246
+ return false;
247
+ }
248
+
249
+ const nextVisible = Boolean(detailsVisible);
250
+ const expandedLabel = button.dataset.expandedLabel || 'Shrink Media Viewer';
251
+ const collapsedLabel = button.dataset.collapsedLabel || 'Show Media Viewer';
252
+ const expandedMarkup = `<span class="material-symbols-outlined">visibility_off</span> ${expandedLabel}`;
253
+
254
+ preview.classList.toggle('media-tagging-preview--meta-hidden', !nextVisible);
255
+ button.dataset.nextValue = nextVisible ? 'false' : 'true';
256
+ button.setAttribute('aria-expanded', nextVisible ? 'true' : 'false');
257
+ button.innerHTML = nextVisible ? expandedMarkup : collapsedLabel;
258
+ return true;
259
+ }
260
+
261
+ function syncMediaTaggingTagSearchUi(input) {
262
+ if (!(input instanceof HTMLInputElement)) {
263
+ return false;
264
+ }
265
+
266
+ const tagPanel = input.closest('.media-tagging-tag-panel');
267
+
268
+ if (!(tagPanel instanceof HTMLElement)) {
269
+ return false;
270
+ }
271
+
272
+ const normalizedQuery = input.value.trim().toLowerCase();
273
+ const tagOptions = tagPanel.querySelectorAll('.media-tagging-tag-option');
274
+
275
+ for (const tagOption of tagOptions) {
276
+ if (!(tagOption instanceof HTMLElement)) {
277
+ continue;
278
+ }
279
+
280
+ const searchText = String(tagOption.dataset.tagSearchText ?? '').trim().toLowerCase();
281
+ tagOption.hidden = Boolean(normalizedQuery) && !searchText.includes(normalizedQuery);
282
+ }
283
+
284
+ return true;
285
+ }
286
+
287
+ function syncDataRowSelectionUi(selectedRowIndex = null) {
288
+ if (getState().route.name !== 'data') {
289
+ return false;
290
+ }
291
+
292
+ const rowNodes = shellRefs.view.querySelectorAll('[data-action="select-data-row"][data-row-index]');
293
+
294
+ for (const rowNode of rowNodes) {
295
+ if (!(rowNode instanceof HTMLElement)) {
296
+ continue;
297
+ }
298
+
299
+ rowNode.classList.toggle('is-selected', rowNode.dataset.rowIndex === String(selectedRowIndex));
300
+ }
301
+
302
+ const panelMarkup = renderDataRowEditorPanel(getState());
303
+ const panelOpen = Boolean(panelMarkup);
304
+
305
+ shellRefs.panel.innerHTML = panelMarkup;
306
+ shellRefs.shell.classList.toggle('panel-open', panelOpen);
307
+ lastRenderedPanelMarkup = panelMarkup;
308
+ lastRenderedPanelOpen = panelOpen;
309
+ return true;
310
+ }
311
+
312
+ function syncQueryHistoryUi(historyId) {
313
+ const state = getState();
314
+ const numericId = Number(historyId);
315
+
316
+ if (!Number.isInteger(numericId) || numericId < 1) {
317
+ return false;
318
+ }
319
+
320
+ const historyItem = state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
321
+ const listItemNode = shellRefs.view.querySelector(
322
+ `[data-action="select-query-history-item"][data-history-id="${String(numericId)}"]`,
323
+ )?.closest('.query-history-item');
324
+
325
+ if (historyItem && listItemNode instanceof HTMLElement) {
326
+ listItemNode.outerHTML = renderQueryHistoryListItem(
327
+ historyItem,
328
+ state.editor.historyActiveId,
329
+ state.editor.historySelectedId,
330
+ );
331
+ }
332
+
333
+ if (state.editor.historySelectedId === numericId) {
334
+ const panelMarkup = renderQueryHistoryDetail({
335
+ item: state.editor.historyDetail,
336
+ runs: state.editor.historyRuns,
337
+ loading: state.editor.historyDetailLoading,
338
+ error: state.editor.historyDetailError,
339
+ });
340
+ const panelOpen = Boolean(panelMarkup);
341
+
342
+ shellRefs.panel.innerHTML = panelMarkup;
343
+ shellRefs.shell.classList.toggle('panel-open', panelOpen);
344
+ lastRenderedPanelMarkup = panelMarkup;
345
+ lastRenderedPanelOpen = panelOpen;
346
+ }
347
+
348
+ return true;
349
+ }
174
350
 
175
- reader.readAsDataURL(file);
176
- });
351
+ function syncQueryHistorySelectionUi(selectedHistoryId = null) {
352
+ const historyButtons = shellRefs.view.querySelectorAll(
353
+ '[data-action="select-query-history-item"][data-history-id]',
354
+ );
355
+
356
+ for (const button of historyButtons) {
357
+ if (!(button instanceof HTMLElement) || !button.classList.contains('query-history-icon-button')) {
358
+ continue;
359
+ }
360
+
361
+ button.classList.toggle('is-active', button.dataset.historyId === String(selectedHistoryId));
362
+ }
363
+
364
+ if (selectedHistoryId === null) {
365
+ shellRefs.panel.innerHTML = '';
366
+ shellRefs.shell.classList.remove('panel-open');
367
+ lastRenderedPanelMarkup = '';
368
+ lastRenderedPanelOpen = false;
369
+ }
370
+
371
+ return true;
372
+ }
373
+
374
+ function readFileAsBase64(file) {
375
+ return new Promise((resolve, reject) => {
376
+ const reader = new FileReader();
377
+
378
+ reader.onload = () => {
379
+ const result = String(reader.result ?? '');
380
+ const markerIndex = result.indexOf(',');
381
+ resolve(markerIndex >= 0 ? result.slice(markerIndex + 1) : result);
382
+ };
383
+ reader.onerror = () => {
384
+ reject(reader.error ?? new Error('The selected logo could not be read.'));
385
+ };
386
+
387
+ reader.readAsDataURL(file);
388
+ });
177
389
  }
178
390
 
179
391
  function readFileAsText(file) {
180
- return new Promise((resolve, reject) => {
181
- const reader = new FileReader();
392
+ return new Promise((resolve, reject) => {
393
+ const reader = new FileReader();
182
394
 
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
- };
395
+ reader.onload = () => {
396
+ resolve(String(reader.result ?? ''));
397
+ };
398
+ reader.onerror = () => {
399
+ reject(reader.error ?? new Error('The selected CSV file could not be read.'));
400
+ };
189
401
 
190
- reader.readAsText(file);
191
- });
402
+ reader.readAsText(file);
403
+ });
192
404
  }
193
405
 
194
406
  async function buildConnectionLogoUpload(file) {
195
- if (!(file instanceof File) || !file.size) {
196
- return null;
197
- }
198
-
199
- return {
200
- fileName: file.name,
201
- mimeType: file.type,
202
- base64: await readFileAsBase64(file),
203
- };
407
+ if (!(file instanceof File) || !file.size) {
408
+ return null;
409
+ }
410
+
411
+ return {
412
+ fileName: file.name,
413
+ mimeType: file.type,
414
+ base64: await readFileAsBase64(file),
415
+ };
204
416
  }
205
417
 
206
418
  function renderNotFoundView() {
207
- return {
208
- main: `
419
+ return {
420
+ main: `
209
421
  <section class="landing-view machined-grid px-6">
210
422
  <div class="text-center z-10">
211
423
  <p class="font-mono text-[10px] uppercase tracking-[0.3em] text-primary-container/40">
@@ -215,7 +427,7 @@ function renderNotFoundView() {
215
427
  404_SIGNAL
216
428
  </h1>
217
429
  <button
218
- class="mt-8 bg-primary-container px-6 py-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-on-primary"
430
+ class="standard-button mt-8 px-6 font-headline text-sm"
219
431
  data-action="navigate"
220
432
  data-to="/"
221
433
  type="button"
@@ -225,868 +437,1136 @@ function renderNotFoundView() {
225
437
  </div>
226
438
  </section>
227
439
  `,
228
- panel: "",
229
- };
440
+ panel: '',
441
+ };
230
442
  }
231
443
 
232
444
  function resolveView(state) {
233
- switch (state.route.name) {
234
- case "landing":
235
- return renderLandingView(state);
236
- case "connections":
237
- return renderConnectionsView(state);
238
- case "overview":
239
- return renderOverviewView(state);
240
- case "charts":
241
- return renderChartsView(state);
242
- case "data":
243
- return renderDataView(state);
244
- case "editor":
245
- return renderEditorView(state, { isResultsRoute: false });
246
- case "editorResults":
247
- return renderEditorView(state, { isResultsRoute: true });
248
- case "structure":
249
- return renderStructureView(state);
250
- case "tableDesigner":
251
- return renderTableDesignerView(state);
252
- case "settings":
253
- return renderSettingsView(state);
254
- default:
255
- return renderNotFoundView();
256
- }
445
+ switch (state.route.name) {
446
+ case 'landing':
447
+ return renderLandingView(state);
448
+ case 'connections':
449
+ return renderConnectionsView(state);
450
+ case 'overview':
451
+ return renderOverviewView(state);
452
+ case 'charts':
453
+ return renderChartsView(state);
454
+ case 'data':
455
+ return renderDataView(state);
456
+ case 'editor':
457
+ return renderEditorView(state, { isResultsRoute: false });
458
+ case 'editorResults':
459
+ return renderEditorView(state, { isResultsRoute: true });
460
+ case 'structure':
461
+ return renderStructureView(state);
462
+ case 'tableDesigner':
463
+ return renderTableDesignerView(state);
464
+ case 'mediaTaggingSetup':
465
+ return renderMediaTaggingView(state, { subView: 'setup' });
466
+ case 'mediaTaggingQueue':
467
+ return renderMediaTaggingView(state, { subView: 'queue' });
468
+ case 'settings':
469
+ return renderSettingsView(state);
470
+ default:
471
+ return renderNotFoundView();
472
+ }
257
473
  }
258
474
 
259
475
  function captureFocusedInputState() {
260
- const activeElement = document.activeElement;
261
-
262
- if (
263
- !activeElement ||
264
- !(
265
- activeElement instanceof HTMLInputElement ||
266
- activeElement instanceof HTMLTextAreaElement ||
267
- activeElement instanceof HTMLSelectElement
268
- )
269
- ) {
270
- return null;
271
- }
272
-
273
- const { bind } = activeElement.dataset;
274
- if (!bind) {
275
- return null;
276
- }
277
-
278
- return {
279
- bind,
280
- field: activeElement.dataset.field ?? null,
281
- columnId: activeElement.dataset.columnId ?? null,
282
- selectionStart: activeElement.selectionStart,
283
- selectionEnd: activeElement.selectionEnd,
284
- selectionDirection: activeElement.selectionDirection,
285
- scrollTop: activeElement.scrollTop,
286
- scrollLeft: activeElement.scrollLeft,
287
- };
476
+ const activeElement = document.activeElement;
477
+
478
+ if (
479
+ !activeElement ||
480
+ !(
481
+ activeElement instanceof HTMLInputElement ||
482
+ activeElement instanceof HTMLTextAreaElement ||
483
+ activeElement instanceof HTMLSelectElement
484
+ )
485
+ ) {
486
+ return null;
487
+ }
488
+
489
+ const { bind } = activeElement.dataset;
490
+ if (!bind) {
491
+ return null;
492
+ }
493
+
494
+ return {
495
+ bind,
496
+ field: activeElement.dataset.field ?? null,
497
+ columnId: activeElement.dataset.columnId ?? null,
498
+ selectionStart: activeElement.selectionStart,
499
+ selectionEnd: activeElement.selectionEnd,
500
+ selectionDirection: activeElement.selectionDirection,
501
+ scrollTop: activeElement.scrollTop,
502
+ scrollLeft: activeElement.scrollLeft,
503
+ };
288
504
  }
289
505
 
290
506
  function buildFocusedInputSelectors(snapshot) {
291
- if (!snapshot?.bind) {
292
- return [];
293
- }
507
+ if (!snapshot?.bind) {
508
+ return [];
509
+ }
294
510
 
295
- const selectors = [];
296
- const bindSelector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
511
+ const selectors = [];
512
+ const bindSelector = `[data-bind="${CSS.escape(snapshot.bind)}"]`;
297
513
 
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
- }
514
+ if (snapshot.columnId && snapshot.field) {
515
+ selectors.push(
516
+ `${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"][data-field="${CSS.escape(snapshot.field)}"]`,
517
+ );
518
+ }
303
519
 
304
- if (snapshot.field) {
305
- selectors.push(`${bindSelector}[data-field="${CSS.escape(snapshot.field)}"]`);
306
- }
520
+ if (snapshot.field) {
521
+ selectors.push(`${bindSelector}[data-field="${CSS.escape(snapshot.field)}"]`);
522
+ }
307
523
 
308
- if (snapshot.columnId) {
309
- selectors.push(`${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"]`);
310
- }
524
+ if (snapshot.columnId) {
525
+ selectors.push(`${bindSelector}[data-column-id="${CSS.escape(snapshot.columnId)}"]`);
526
+ }
311
527
 
312
- selectors.push(bindSelector);
313
- return selectors;
528
+ selectors.push(bindSelector);
529
+ return selectors;
314
530
  }
315
531
 
316
532
  function restoreFocusedInputState(snapshot) {
317
- if (!snapshot) {
318
- return false;
319
- }
320
-
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
- );
533
+ if (!snapshot) {
534
+ return false;
535
+ }
329
536
 
330
- if (
331
- !nextElement ||
332
- !(
333
- nextElement instanceof HTMLInputElement ||
334
- nextElement instanceof HTMLTextAreaElement ||
335
- nextElement instanceof HTMLSelectElement
336
- )
337
- ) {
338
- return false;
339
- }
340
-
341
- nextElement.focus({ preventScroll: true });
342
-
343
- if (
344
- (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) &&
345
- typeof snapshot.selectionStart === "number" &&
346
- typeof snapshot.selectionEnd === "number"
347
- ) {
348
- nextElement.setSelectionRange(
349
- snapshot.selectionStart,
350
- snapshot.selectionEnd,
351
- snapshot.selectionDirection || "none"
352
- );
353
- }
537
+ const nextElement = buildFocusedInputSelectors(snapshot)
538
+ .map(selector => document.querySelector(selector))
539
+ .find(
540
+ candidate =>
541
+ candidate instanceof HTMLInputElement ||
542
+ candidate instanceof HTMLTextAreaElement ||
543
+ candidate instanceof HTMLSelectElement,
544
+ );
354
545
 
355
- nextElement.scrollTop = snapshot.scrollTop;
356
- nextElement.scrollLeft = snapshot.scrollLeft;
357
- return true;
546
+ if (
547
+ !nextElement ||
548
+ !(
549
+ nextElement instanceof HTMLInputElement ||
550
+ nextElement instanceof HTMLTextAreaElement ||
551
+ nextElement instanceof HTMLSelectElement
552
+ )
553
+ ) {
554
+ return false;
555
+ }
556
+
557
+ nextElement.focus({ preventScroll: true });
558
+
559
+ if (
560
+ (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) &&
561
+ typeof snapshot.selectionStart === 'number' &&
562
+ typeof snapshot.selectionEnd === 'number'
563
+ ) {
564
+ nextElement.setSelectionRange(
565
+ snapshot.selectionStart,
566
+ snapshot.selectionEnd,
567
+ snapshot.selectionDirection || 'none',
568
+ );
569
+ }
570
+
571
+ nextElement.scrollTop = snapshot.scrollTop;
572
+ nextElement.scrollLeft = snapshot.scrollLeft;
573
+ return true;
358
574
  }
359
575
 
360
576
  function focusNewTableDesignerNameField() {
361
- const input = document.querySelector(
362
- '[data-bind="table-designer-field"][data-field="tableName"]'
363
- );
577
+ const input = document.querySelector('[data-bind="table-designer-field"][data-field="tableName"]');
364
578
 
365
- if (!(input instanceof HTMLInputElement)) {
366
- return false;
367
- }
579
+ if (!(input instanceof HTMLInputElement)) {
580
+ return false;
581
+ }
368
582
 
369
- input.focus({ preventScroll: true });
370
- input.setSelectionRange(input.value.length, input.value.length);
371
- return true;
583
+ input.focus({ preventScroll: true });
584
+ input.setSelectionRange(input.value.length, input.value.length);
585
+ return true;
372
586
  }
373
587
 
374
588
  function focusTableDesignerColumnNameField(columnId) {
375
- if (!columnId) {
376
- return false;
377
- }
589
+ if (!columnId) {
590
+ return false;
591
+ }
378
592
 
379
- const input = document.querySelector(
380
- `[data-bind="table-designer-column-field"][data-column-id="${CSS.escape(columnId)}"][data-field="name"]`
381
- );
593
+ const input = document.querySelector(
594
+ `[data-bind="table-designer-column-field"][data-column-id="${CSS.escape(columnId)}"][data-field="name"]`,
595
+ );
382
596
 
383
- if (!(input instanceof HTMLInputElement)) {
384
- return false;
385
- }
597
+ if (!(input instanceof HTMLInputElement)) {
598
+ return false;
599
+ }
386
600
 
387
- input.focus({ preventScroll: true });
388
- input.setSelectionRange(input.value.length, input.value.length);
389
- return true;
601
+ input.focus({ preventScroll: true });
602
+ input.setSelectionRange(input.value.length, input.value.length);
603
+ return true;
390
604
  }
391
605
 
392
- async function handleTableDesignerCsvImport(fileInput) {
393
- if (!(fileInput instanceof HTMLInputElement)) {
394
- return;
395
- }
606
+ function focusQueryEditorInput() {
607
+ const input = document.querySelector('[data-bind="current-query"]');
396
608
 
397
- const file = fileInput.files?.[0];
609
+ if (!(input instanceof HTMLTextAreaElement)) {
610
+ return false;
611
+ }
398
612
 
399
- if (!(file instanceof File)) {
400
- return;
401
- }
613
+ input.focus({ preventScroll: true });
614
+ input.setSelectionRange(input.value.length, input.value.length);
615
+ return true;
616
+ }
402
617
 
403
- if (!file.size) {
404
- showToast("The selected CSV file is empty.", "alert");
405
- fileInput.value = "";
406
- return;
407
- }
618
+ async function handleTableDesignerCsvImport(fileInput) {
619
+ if (!(fileInput instanceof HTMLInputElement)) {
620
+ return;
621
+ }
408
622
 
409
- try {
410
- const csvText = await readFileAsText(file);
411
- const imported = queueTableDesignerCsvImport(file.name, csvText);
623
+ const file = fileInput.files?.[0];
412
624
 
413
- if (!imported) {
414
- return;
625
+ if (!(file instanceof File)) {
626
+ return;
415
627
  }
416
628
 
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
- }
629
+ if (!file.size) {
630
+ showToast('The selected CSV file is empty.', 'alert');
631
+ fileInput.value = '';
632
+ return;
633
+ }
634
+
635
+ try {
636
+ const csvText = await readFileAsText(file);
637
+ const imported = queueTableDesignerCsvImport(file.name, csvText);
638
+
639
+ if (!imported) {
640
+ return;
641
+ }
642
+
643
+ router.navigate('/table-designer/new');
644
+ showToast(
645
+ `Imported ${imported.columnCount} columns and ${imported.importedRowCount} row${
646
+ imported.importedRowCount === 1 ? '' : 's'
647
+ } from ${file.name}.`,
648
+ 'success',
649
+ );
650
+ } catch (error) {
651
+ showToast(error?.message || 'CSV import failed.', 'alert');
652
+ } finally {
653
+ fileInput.value = '';
654
+ }
429
655
  }
430
656
 
431
657
  function renderApp(state) {
432
- const focusedInput = captureFocusedInputState();
433
- const previousRoutePath = lastRenderedRoutePath;
434
- const { main, panel } = resolveView(state);
435
- const isLockedRoute = ["editor", "editorResults", "data", "charts", "structure", "tableDesigner"].includes(
436
- state.route.name
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
- }
448
-
449
- teardownStructureGraph();
450
- teardownQueryChartRenderer();
451
- shellRefs.topNav.innerHTML = renderTopNav(state);
452
- shellRefs.sidebar.innerHTML = renderSidebar(state);
453
- shellRefs.statusBar.innerHTML = renderStatusBar(state);
454
- shellRefs.view.innerHTML = main;
455
- shellRefs.view.classList.toggle("app-main-scroll--locked", isLockedRoute);
456
- shellRefs.panel.innerHTML = panel;
457
- shellRefs.modal.innerHTML = renderModal(state);
458
- shellRefs.toast.innerHTML = renderToasts(state.toasts);
459
- shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
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;
475
-
476
- if (state.route.name === "structure") {
477
- mountStructureGraph(state).catch((error) => {
478
- console.error("Failed to mount structure graph.", error);
479
- });
480
- }
658
+ const previousRoutePath = lastRenderedRoutePath;
659
+ const { main, panel } = resolveView(state);
660
+ const topNavMarkup = renderTopNav(state);
661
+ const sidebarMarkup = renderSidebar(state);
662
+ const statusBarMarkup = renderStatusBar(state);
663
+ const modalMarkup = renderModal(state);
664
+ const toastMarkup = renderToasts(state.toasts);
665
+ const isLockedRoute = [
666
+ 'editor',
667
+ 'editorResults',
668
+ 'data',
669
+ 'charts',
670
+ 'structure',
671
+ 'tableDesigner',
672
+ 'mediaTaggingSetup',
673
+ 'mediaTaggingQueue',
674
+ ].includes(state.route.name);
675
+ const panelOpen = Boolean(panel);
676
+ const topNavChanged = topNavMarkup !== lastRenderedTopNavMarkup;
677
+ const sidebarChanged = sidebarMarkup !== lastRenderedSidebarMarkup;
678
+ const statusBarChanged = statusBarMarkup !== lastRenderedStatusBarMarkup;
679
+ const mainChanged = main !== lastRenderedMainMarkup;
680
+ const panelChanged = panel !== lastRenderedPanelMarkup;
681
+ const modalChanged = modalMarkup !== lastRenderedModalMarkup;
682
+ const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
683
+ const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
684
+ const shellMarkupUnchanged =
685
+ state.route.path === lastRenderedRoutePath &&
686
+ !topNavChanged &&
687
+ !sidebarChanged &&
688
+ !statusBarChanged &&
689
+ !mainChanged &&
690
+ !panelChanged &&
691
+ !modalChanged &&
692
+ !panelOpenChanged &&
693
+ !lockedRouteChanged;
694
+
695
+ if (shellMarkupUnchanged && toastMarkup !== lastRenderedToastMarkup) {
696
+ shellRefs.toast.innerHTML = toastMarkup;
697
+ lastRenderedToastMarkup = toastMarkup;
698
+ return;
699
+ }
700
+
701
+ if (shellMarkupUnchanged && toastMarkup === lastRenderedToastMarkup) {
702
+ return;
703
+ }
481
704
 
482
- if (state.route.name === "charts") {
483
- mountQueryChartRenderer(state);
484
- }
705
+ const focusedInput = captureFocusedInputState();
706
+ const isEnteringNewTableDesignerRoute =
707
+ state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
708
+
709
+ if (isEnteringNewTableDesignerRoute) {
710
+ pendingNewTableDesignerAutofocus = true;
711
+ } else if (state.route.name !== 'tableDesigner' || !state.route.params?.isNew) {
712
+ pendingNewTableDesignerAutofocus = false;
713
+ }
714
+
715
+ if (mainChanged) {
716
+ teardownStructureGraph();
717
+ teardownQueryChartRenderer();
718
+ }
719
+
720
+ if (topNavChanged) {
721
+ shellRefs.topNav.innerHTML = topNavMarkup;
722
+ }
723
+
724
+ if (sidebarChanged) {
725
+ shellRefs.sidebar.innerHTML = sidebarMarkup;
726
+ }
727
+
728
+ if (statusBarChanged) {
729
+ shellRefs.statusBar.innerHTML = statusBarMarkup;
730
+ }
731
+
732
+ if (mainChanged) {
733
+ shellRefs.view.innerHTML = main;
734
+ }
735
+
736
+ if (mainChanged || lockedRouteChanged) {
737
+ shellRefs.view.classList.toggle('app-main-scroll--locked', isLockedRoute);
738
+ }
739
+
740
+ if (panelChanged) {
741
+ shellRefs.panel.innerHTML = panel;
742
+ }
743
+
744
+ if (modalChanged) {
745
+ shellRefs.modal.innerHTML = modalMarkup;
746
+ }
747
+
748
+ if (toastMarkup !== lastRenderedToastMarkup) {
749
+ shellRefs.toast.innerHTML = toastMarkup;
750
+ }
751
+
752
+ if (panelChanged || panelOpenChanged) {
753
+ shellRefs.shell.classList.toggle('panel-open', panelOpen);
754
+ }
755
+
756
+ if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
757
+ if (focusQueryEditorInput()) {
758
+ pendingQueryEditorFocus = false;
759
+ }
760
+ } else if (
761
+ pendingNewTableDesignerAutofocus &&
762
+ state.route.name === 'tableDesigner' &&
763
+ state.route.params?.isNew &&
764
+ state.tableDesigner.draft?.mode === 'create'
765
+ ) {
766
+ if (focusNewTableDesignerNameField()) {
767
+ pendingNewTableDesignerAutofocus = false;
768
+ }
769
+ } else {
770
+ restoreFocusedInputState(focusedInput);
771
+ }
772
+
773
+ lastRenderedRoutePath = state.route.path;
774
+ lastRenderedTopNavMarkup = topNavMarkup;
775
+ lastRenderedSidebarMarkup = sidebarMarkup;
776
+ lastRenderedStatusBarMarkup = statusBarMarkup;
777
+ lastRenderedMainMarkup = main;
778
+ lastRenderedPanelMarkup = panel;
779
+ lastRenderedModalMarkup = modalMarkup;
780
+ lastRenderedToastMarkup = toastMarkup;
781
+ lastRenderedPanelOpen = panelOpen;
782
+ lastRenderedLockedRoute = isLockedRoute;
783
+
784
+ if (state.route.name === 'structure') {
785
+ mountStructureGraph(state).catch(error => {
786
+ console.error('Failed to mount structure graph.', error);
787
+ });
788
+ }
789
+
790
+ if (state.route.name === 'charts') {
791
+ mountQueryChartRenderer(state);
792
+ }
485
793
  }
486
794
 
487
- const router = createRouter((route) => {
488
- setRoute(route);
795
+ const router = createRouter(route => {
796
+ setRoute(route);
489
797
  });
490
798
 
491
799
  async function executeEditorQueryAndNavigate() {
492
- const success = await executeCurrentQuery();
493
- router.navigate(success ? "/editor/results" : "/editor");
800
+ const success = await executeCurrentQuery();
801
+ router.navigate(success ? '/editor/results' : '/editor');
494
802
  }
495
803
 
496
804
  async function handleAction(actionNode) {
497
- const { action } = actionNode.dataset;
498
-
499
- switch (action) {
500
- case "navigate":
501
- router.navigate(actionNode.dataset.to ?? "/");
502
- return;
503
- case "refresh-view":
504
- await refreshCurrentRoute();
505
- return;
506
- case "open-modal":
507
- openModal(actionNode.dataset.modal);
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;
518
- case "edit-connection":
519
- openEditConnectionModal(actionNode.dataset.connectionId);
520
- return;
521
- case "close-modal":
522
- closeModal();
523
- return;
524
- case "dismiss-toast":
525
- dismissToast(actionNode.dataset.toastId);
526
- return;
527
- case "select-connection": {
528
- resetStructureGraphForDatabaseChange();
529
- const next = await selectConnection(actionNode.dataset.connectionId);
530
- if (next) {
531
- router.navigate("/overview");
532
- }
533
- return;
534
- }
535
- case "remove-connection": {
536
- const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
537
-
538
- if (isActiveConnection) {
539
- resetStructureGraphForDatabaseChange();
540
- }
541
-
542
- const removed = await removeConnection(actionNode.dataset.connectionId);
543
- if (removed) {
544
- const nextState = getState();
545
- if (!nextState.connections.active && nextState.route.name !== "connections") {
546
- router.navigate("/connections");
547
- } else {
548
- await refreshCurrentRoute();
805
+ const { action } = actionNode.dataset;
806
+
807
+ switch (action) {
808
+ case 'navigate':
809
+ router.navigate(actionNode.dataset.to ?? '/');
810
+ return;
811
+ case 'refresh-view':
812
+ await refreshCurrentRoute();
813
+ return;
814
+ case 'open-modal':
815
+ openModal(actionNode.dataset.modal);
816
+ return;
817
+ case 'open-create-query-chart-modal':
818
+ openCreateQueryChartModal();
819
+ return;
820
+ case 'open-edit-query-chart-modal':
821
+ openEditQueryChartModal(actionNode.dataset.chartId);
822
+ return;
823
+ case 'open-delete-query-chart-modal':
824
+ openDeleteQueryChartModal(actionNode.dataset.chartId);
825
+ return;
826
+ case 'open-delete-query-history-modal':
827
+ openDeleteQueryHistoryModal(actionNode.dataset.historyId);
828
+ return;
829
+ case 'edit-connection':
830
+ openEditConnectionModal(actionNode.dataset.connectionId);
831
+ return;
832
+ case 'close-modal':
833
+ closeModal();
834
+ return;
835
+ case 'dismiss-toast':
836
+ dismissToast(actionNode.dataset.toastId);
837
+ return;
838
+ case 'select-connection': {
839
+ resetStructureGraphForDatabaseChange();
840
+ const next = await selectConnection(actionNode.dataset.connectionId);
841
+ if (next) {
842
+ router.navigate('/overview');
843
+ }
844
+ return;
549
845
  }
550
- }
551
- return;
552
- }
553
- case "create-backup":
554
- await createActiveConnectionBackup();
555
- return;
556
- case "open-overview-in-finder":
557
- await openOverviewInFinder();
558
- return;
559
- case "execute-query": {
560
- await executeEditorQueryAndNavigate();
561
- return;
562
- }
563
- case "delete-data-row":
564
- openDeleteDataRowModal(actionNode.dataset.rowIndex);
565
- return;
566
- case "delete-editor-row":
567
- openDeleteEditorRowModal(actionNode.dataset.rowIndex);
568
- return;
569
- case "clear-query":
570
- clearCurrentQuery();
571
- if (getState().route.name === "editorResults") {
572
- router.navigate("/editor");
573
- }
574
- return;
575
- case "clear-results":
576
- clearEditorResults();
577
- router.navigate("/editor");
578
- return;
579
- case "select-query-history-item":
580
- if (actionNode.dataset.historyId) {
581
- await selectQueryHistoryItem(actionNode.dataset.historyId);
582
- }
583
- return;
584
- case "clear-query-history-selection":
585
- clearQueryHistorySelection();
586
- return;
587
- case "set-query-history-tab":
588
- if (actionNode.dataset.tab) {
589
- await setQueryHistoryTab(actionNode.dataset.tab);
590
- }
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;
602
- case "load-more-query-history":
603
- await loadMoreQueryHistory();
604
- return;
605
- case "open-query-history":
606
- if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
607
- router.navigate("/editor");
608
- }
609
- return;
610
- case "run-query-history":
611
- if (actionNode.dataset.historyId) {
612
- const success = await runQueryHistoryItem(actionNode.dataset.historyId);
613
- router.navigate(success ? "/editor/results" : "/editor");
614
- }
615
- return;
616
- case "toggle-query-history-saved":
617
- if (actionNode.dataset.historyId) {
618
- await toggleQueryHistorySavedState(
619
- actionNode.dataset.historyId,
620
- actionNode.dataset.nextValue === "true"
621
- );
622
- }
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;
640
- case "delete-query-history":
641
- if (
642
- actionNode.dataset.historyId &&
643
- window.confirm("Delete this query history item and all recorded runs?")
644
- ) {
645
- await deleteQueryHistoryStateItem(actionNode.dataset.historyId);
646
- }
647
- return;
648
- case "set-editor-tab": {
649
- const tab = actionNode.dataset.tab;
650
- if (!tab) {
846
+ case 'remove-connection': {
847
+ const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
848
+
849
+ if (isActiveConnection) {
850
+ resetStructureGraphForDatabaseChange();
851
+ }
852
+
853
+ const removed = await removeConnection(actionNode.dataset.connectionId);
854
+ if (removed) {
855
+ const nextState = getState();
856
+ if (!nextState.connections.active && nextState.route.name !== 'connections') {
857
+ router.navigate('/connections');
858
+ } else {
859
+ await refreshCurrentRoute();
860
+ }
861
+ }
862
+ return;
863
+ }
864
+ case 'create-backup':
865
+ await createActiveConnectionBackup();
866
+ return;
867
+ case 'copy-media-tags': {
868
+ const tags = getState().mediaTagging.tags ?? [];
869
+ const formattedTags = tags
870
+ .map(tag => `${String(tag.label ?? '').trim()}${tag.isParentTag ? ' (parent)' : ''}`)
871
+ .filter(Boolean)
872
+ .join(', ');
873
+
874
+ if (!formattedTags) {
875
+ showToast('No tags available to copy.', 'alert');
876
+ return;
877
+ }
878
+
879
+ try {
880
+ await navigator.clipboard.writeText(formattedTags);
881
+ showToast('Tags copied.', 'success');
882
+ } catch (error) {
883
+ showToast('Tags could not be copied.', 'alert');
884
+ }
885
+ return;
886
+ }
887
+ case 'open-overview-in-finder':
888
+ await openOverviewInFinder();
889
+ return;
890
+ case 'execute-query': {
891
+ await executeEditorQueryAndNavigate();
892
+ return;
893
+ }
894
+ case 'delete-data-row':
895
+ openDeleteDataRowModal(actionNode.dataset.rowIndex);
896
+ return;
897
+ case 'delete-editor-row':
898
+ openDeleteEditorRowModal(actionNode.dataset.rowIndex);
899
+ return;
900
+ case 'clear-query':
901
+ pendingQueryEditorFocus = true;
902
+ clearCurrentQuery();
903
+ if (getState().route.name === 'editorResults') {
904
+ router.navigate('/editor');
905
+ }
906
+ return;
907
+ case 'clear-results':
908
+ clearEditorResults();
909
+ router.navigate('/editor');
910
+ return;
911
+ case 'select-query-history-item':
912
+ if (actionNode.dataset.historyId) {
913
+ const pendingSelection = selectQueryHistoryItem(actionNode.dataset.historyId, { notify: false });
914
+ syncQueryHistorySelectionUi(actionNode.dataset.historyId);
915
+ syncQueryHistoryUi(actionNode.dataset.historyId);
916
+ await pendingSelection;
917
+ syncQueryHistoryUi(actionNode.dataset.historyId);
918
+ }
919
+ return;
920
+ case 'clear-query-history-selection':
921
+ clearQueryHistorySelection({ notify: false });
922
+ syncQueryHistorySelectionUi(null);
923
+ return;
924
+ case 'set-query-history-tab':
925
+ if (actionNode.dataset.tab) {
926
+ await setQueryHistoryTab(actionNode.dataset.tab);
927
+ }
928
+ return;
929
+ case 'toggle-query-history-panel':
930
+ setQueryHistoryPanelVisibility(
931
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
932
+ );
933
+ return;
934
+ case 'toggle-editor-panel':
935
+ setEditorPanelVisibility(
936
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
937
+ );
938
+ return;
939
+ case 'load-more-query-history':
940
+ await loadMoreQueryHistory();
941
+ return;
942
+ case 'open-query-history':
943
+ if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
944
+ router.navigate('/editor');
945
+ }
946
+ return;
947
+ case 'run-query-history':
948
+ if (actionNode.dataset.historyId) {
949
+ const success = await runQueryHistoryItem(actionNode.dataset.historyId);
950
+ router.navigate(success ? '/editor/results' : '/editor');
951
+ }
952
+ return;
953
+ case 'toggle-query-history-saved':
954
+ if (actionNode.dataset.historyId) {
955
+ await toggleQueryHistorySavedState(
956
+ actionNode.dataset.historyId,
957
+ actionNode.dataset.nextValue === 'true',
958
+ );
959
+ }
960
+ return;
961
+ case 'toggle-charts-sql-panel':
962
+ toggleChartsSqlPanel();
963
+ return;
964
+ case 'toggle-charts-results-panel':
965
+ toggleChartsResultsPanel();
966
+ return;
967
+ case 'set-charts-height-preset':
968
+ if (actionNode.dataset.preset) {
969
+ setChartsHeightPreset(actionNode.dataset.preset);
970
+ }
971
+ return;
972
+ case 'export-query-chart-png':
973
+ if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
974
+ showToast('The selected chart is not ready for PNG export.', 'alert');
975
+ }
976
+ return;
977
+ case 'set-editor-tab': {
978
+ const tab = actionNode.dataset.tab;
979
+ if (!tab) {
980
+ return;
981
+ }
982
+ setEditorTab(tab);
983
+ router.navigate(tab === 'results' ? '/editor/results' : '/editor');
984
+ return;
985
+ }
986
+ case 'export-query-csv':
987
+ await exportCurrentQueryCsv();
988
+ return;
989
+ case 'export-data-csv':
990
+ await exportCurrentDataTableCsv();
991
+ return;
992
+ case 'toggle-data-tables':
993
+ toggleDataTablesPanel();
994
+ return;
995
+ case 'toggle-structure-tables':
996
+ toggleStructureTablesPanel();
997
+ return;
998
+ case 'select-structure-entry':
999
+ if (actionNode.dataset.entryName) {
1000
+ await selectStructureEntry(actionNode.dataset.entryName);
1001
+ }
1002
+ return;
1003
+ case 'add-table-designer-column':
1004
+ {
1005
+ const columnId = addCurrentTableDesignerColumn();
1006
+ if (columnId) {
1007
+ window.requestAnimationFrame(() => {
1008
+ focusTableDesignerColumnNameField(columnId);
1009
+ });
1010
+ }
1011
+ }
1012
+ return;
1013
+ case 'remove-table-designer-column':
1014
+ if (actionNode.dataset.columnId) {
1015
+ removeCurrentTableDesignerColumn(actionNode.dataset.columnId);
1016
+ }
1017
+ return;
1018
+ case 'save-table-designer': {
1019
+ const savedTableName = await saveCurrentTableDesignerDraft();
1020
+ if (savedTableName) {
1021
+ router.navigate(`/table-designer/${encodeURIComponent(savedTableName)}`);
1022
+ }
1023
+ return;
1024
+ }
1025
+ case 'copy-table-designer-sql': {
1026
+ const sqlPreview = getState().tableDesigner.draft?.sqlPreview ?? '';
1027
+ if (!sqlPreview.trim()) {
1028
+ return;
1029
+ }
1030
+
1031
+ try {
1032
+ await navigator.clipboard.writeText(sqlPreview);
1033
+ showToast('SQL preview copied.', 'success');
1034
+ } catch (error) {
1035
+ showToast('Clipboard access failed.', 'alert');
1036
+ }
1037
+ return;
1038
+ }
1039
+ case 'toggle-table-designer-sql-preview':
1040
+ setTableDesignerSqlPreviewVisibility(
1041
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === 'true' : undefined,
1042
+ );
1043
+ return;
1044
+ case 'refresh-media-tagging-preview':
1045
+ await refreshMediaTaggingPreview();
1046
+ return;
1047
+ case 'dismiss-media-tagging-issue':
1048
+ dismissMediaTaggingIssue(actionNode.dataset.issueKey);
1049
+ return;
1050
+ case 'reset-media-tagging-queries':
1051
+ await resetMediaTaggingQueriesToDefault();
1052
+ return;
1053
+ case 'save-media-tagging':
1054
+ await saveCurrentMediaTaggingConfig();
1055
+ return;
1056
+ case 'create-media-tag':
1057
+ await createCurrentMediaTag();
1058
+ // Refocus the name field after creating a tag
1059
+ const nameInput = document.querySelector('[data-bind="media-tagging-tag-form-field"][data-field="name"]');
1060
+ if (nameInput instanceof HTMLInputElement) {
1061
+ nameInput.focus();
1062
+ }
1063
+ return;
1064
+ case 'remove-media-tag':
1065
+ await removeCurrentMediaTag(actionNode.dataset.tagKey);
1066
+ return;
1067
+ case 'skip-media-tagging-item':
1068
+ await skipCurrentMediaTaggingItem();
1069
+ return;
1070
+ case 'reset-skipped-media-tagging':
1071
+ await resetSkippedMediaTaggingItems();
1072
+ return;
1073
+ case 'apply-media-tagging':
1074
+ await applyCurrentMediaTaggingSelection();
1075
+ return;
1076
+ case 'toggle-media-tagging-current-media':
1077
+ if (actionNode instanceof HTMLButtonElement) {
1078
+ const nextValue = actionNode.dataset.nextValue !== 'false';
1079
+ syncMediaTaggingCurrentMediaUi(actionNode, nextValue);
1080
+ setMediaTaggingWorkflowMediaDetailsVisible(nextValue, { notify: false });
1081
+ }
1082
+ return;
1083
+ case 'open-media-tagging-current-in-data': {
1084
+ const currentState = getState();
1085
+ const mediaTableName = currentState.mediaTagging.draft?.mediaTable ?? '';
1086
+ const identity = currentState.mediaTagging.workflow?.currentItem?.identity ?? null;
1087
+
1088
+ if (!openDataRowByIdentity(mediaTableName, identity)) {
1089
+ showToast('The current media row could not be opened in Data.', 'alert');
1090
+ return;
1091
+ }
1092
+
1093
+ router.navigate(`/data/${encodeURIComponent(mediaTableName)}`);
1094
+ return;
1095
+ }
1096
+ case 'import-table-designer-csv': {
1097
+ const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
1098
+
1099
+ if (!(fileInput instanceof HTMLInputElement)) {
1100
+ return;
1101
+ }
1102
+
1103
+ fileInput.value = '';
1104
+ fileInput.click();
1105
+ return;
1106
+ }
1107
+ case 'select-data-row':
1108
+ if (actionNode.dataset.rowIndex) {
1109
+ selectDataRow(actionNode.dataset.rowIndex, { notify: false });
1110
+ syncDataRowSelectionUi(actionNode.dataset.rowIndex);
1111
+ }
1112
+ return;
1113
+ case 'select-editor-row':
1114
+ if (actionNode.dataset.rowIndex) {
1115
+ selectEditorRow(actionNode.dataset.rowIndex);
1116
+ }
1117
+ return;
1118
+ case 'clear-data-row-selection':
1119
+ clearDataRowSelection({ notify: false });
1120
+ syncDataRowSelectionUi(null);
1121
+ return;
1122
+ case 'clear-editor-row-selection':
1123
+ clearEditorRowSelection();
1124
+ return;
1125
+ case 'set-data-page':
1126
+ if (actionNode.dataset.page) {
1127
+ await setDataPage(actionNode.dataset.page);
1128
+ }
1129
+ return;
1130
+ case 'sort-data-column':
1131
+ if (actionNode.dataset.columnName) {
1132
+ await sortDataTableByColumn(actionNode.dataset.columnName);
1133
+ }
1134
+ return;
1135
+ case 'sort-editor-results-column':
1136
+ if (actionNode.dataset.columnName) {
1137
+ sortEditorResultsByColumn(actionNode.dataset.columnName);
1138
+ }
1139
+ return;
1140
+ case 'set-data-page-size':
1141
+ if (actionNode.dataset.pageSize) {
1142
+ await setDataPageSize(actionNode.dataset.pageSize);
1143
+ }
1144
+ return;
1145
+ case 'reload-data-route':
1146
+ await refreshCurrentRoute();
1147
+ return;
1148
+ default:
1149
+ }
1150
+ }
1151
+
1152
+ document.addEventListener('click', event => {
1153
+ const actionNode = event.target.closest('[data-action]');
1154
+
1155
+ if (!actionNode) {
651
1156
  return;
652
- }
653
- setEditorTab(tab);
654
- router.navigate(tab === "results" ? "/editor/results" : "/editor");
655
- return;
656
- }
657
- case "export-query-csv":
658
- await exportCurrentQueryCsv();
659
- return;
660
- case "export-data-csv":
661
- await exportCurrentDataTableCsv();
662
- return;
663
- case "toggle-data-tables":
664
- toggleDataTablesPanel();
665
- return;
666
- case "select-structure-entry":
667
- if (actionNode.dataset.entryName) {
668
- await selectStructureEntry(actionNode.dataset.entryName);
669
- }
670
- return;
671
- case "add-table-designer-column":
672
- {
673
- const columnId = addCurrentTableDesignerColumn();
674
- if (columnId) {
675
- window.requestAnimationFrame(() => {
676
- focusTableDesignerColumnNameField(columnId);
677
- });
1157
+ }
1158
+
1159
+ handleAction(actionNode);
1160
+ });
1161
+
1162
+ document.addEventListener('keydown', event => {
1163
+ const target = event.target;
1164
+
1165
+ // Handle Enter key in tag form fields to trigger create tag
1166
+ if (
1167
+ event.key === 'Enter' &&
1168
+ !event.shiftKey &&
1169
+ !event.altKey &&
1170
+ !event.ctrlKey &&
1171
+ !event.metaKey &&
1172
+ !event.defaultPrevented &&
1173
+ target instanceof HTMLElement &&
1174
+ target.dataset.bind === 'media-tagging-tag-form-field'
1175
+ ) {
1176
+ event.preventDefault();
1177
+ const createButton = document.querySelector('[data-action="create-media-tag"]');
1178
+ if (createButton && !(createButton instanceof HTMLButtonElement && createButton.disabled)) {
1179
+ createButton?.click();
678
1180
  }
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
1181
  return;
697
- }
1182
+ }
698
1183
 
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;
1184
+ if (
1185
+ event.key === 'Enter' &&
1186
+ event.shiftKey &&
1187
+ !event.altKey &&
1188
+ !event.ctrlKey &&
1189
+ !event.metaKey &&
1190
+ !event.defaultPrevented &&
1191
+ target instanceof HTMLTextAreaElement &&
1192
+ target.dataset.bind === 'current-query'
1193
+ ) {
1194
+ event.preventDefault();
1195
+
1196
+ if (!getState().editor.executing) {
1197
+ void executeEditorQueryAndNavigate();
1198
+ }
1199
+
1200
+ return;
706
1201
  }
707
- case "import-table-designer-csv": {
708
- const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
709
1202
 
710
- if (!(fileInput instanceof HTMLInputElement)) {
1203
+ if (event.key !== 'Escape' || event.defaultPrevented) {
711
1204
  return;
712
- }
713
-
714
- fileInput.value = "";
715
- fileInput.click();
716
- return;
717
- }
718
- case "select-data-row":
719
- if (actionNode.dataset.rowIndex) {
720
- selectDataRow(actionNode.dataset.rowIndex);
721
- }
722
- return;
723
- case "select-editor-row":
724
- if (actionNode.dataset.rowIndex) {
725
- selectEditorRow(actionNode.dataset.rowIndex);
726
- }
727
- return;
728
- case "clear-data-row-selection":
729
- clearDataRowSelection();
730
- return;
731
- case "clear-editor-row-selection":
732
- clearEditorRowSelection();
733
- return;
734
- case "set-data-page":
735
- if (actionNode.dataset.page) {
736
- await setDataPage(actionNode.dataset.page);
737
- }
738
- return;
739
- case "sort-data-column":
740
- if (actionNode.dataset.columnName) {
741
- await sortDataTableByColumn(actionNode.dataset.columnName);
742
- }
743
- return;
744
- case "sort-editor-results-column":
745
- if (actionNode.dataset.columnName) {
746
- sortEditorResultsByColumn(actionNode.dataset.columnName);
747
- }
748
- return;
749
- case "set-data-page-size":
750
- if (actionNode.dataset.pageSize) {
751
- await setDataPageSize(actionNode.dataset.pageSize);
752
- }
753
- return;
754
- case "reload-data-route":
755
- await refreshCurrentRoute();
756
- return;
757
- default:
758
- }
759
- }
1205
+ }
760
1206
 
761
- document.addEventListener("click", (event) => {
762
- const actionNode = event.target.closest("[data-action]");
1207
+ const state = getState();
763
1208
 
764
- if (!actionNode) {
765
- return;
766
- }
1209
+ if (state.modal) {
1210
+ event.preventDefault();
1211
+ closeModal();
1212
+ return;
1213
+ }
1214
+
1215
+ if (
1216
+ state.route.name === 'data' &&
1217
+ (typeof state.dataBrowser.selectedRowIndex === 'number' || Boolean(state.dataBrowser.selectedRow))
1218
+ ) {
1219
+ event.preventDefault();
1220
+ clearDataRowSelection();
1221
+ return;
1222
+ }
767
1223
 
768
- handleAction(actionNode);
1224
+ if (state.route.name === 'editorResults' && typeof state.editor.selectedRowIndex === 'number') {
1225
+ event.preventDefault();
1226
+ clearEditorRowSelection();
1227
+ }
769
1228
  });
770
1229
 
771
- document.addEventListener("keydown", (event) => {
772
- const target = event.target;
773
-
774
- if (
775
- event.key === "Enter" &&
776
- event.shiftKey &&
777
- !event.altKey &&
778
- !event.ctrlKey &&
779
- !event.metaKey &&
780
- !event.defaultPrevented &&
781
- target instanceof HTMLTextAreaElement &&
782
- target.dataset.bind === "current-query"
783
- ) {
784
- event.preventDefault();
1230
+ document.addEventListener('input', event => {
1231
+ const bindNode = event.target.closest('[data-bind]');
785
1232
 
786
- if (!getState().editor.executing) {
787
- void executeEditorQueryAndNavigate();
1233
+ if (!bindNode) {
1234
+ return;
788
1235
  }
789
1236
 
790
- return;
791
- }
792
-
793
- if (event.key !== "Escape" || event.defaultPrevented) {
794
- return;
795
- }
1237
+ if (bindNode.dataset.bind === 'current-query') {
1238
+ invalidateMainRenderCache();
1239
+ syncQueryEditorHighlight(bindNode);
1240
+ syncQueryEditorScroll(bindNode);
1241
+ setCurrentQuery(bindNode.value);
1242
+ return;
1243
+ }
796
1244
 
797
- const state = getState();
1245
+ if (bindNode.dataset.bind === 'data-search-query') {
1246
+ setDataSearchQuery(bindNode.value);
1247
+ return;
1248
+ }
798
1249
 
799
- if (state.modal) {
800
- event.preventDefault();
801
- closeModal();
802
- return;
803
- }
1250
+ if (bindNode.dataset.bind === 'table-designer-search') {
1251
+ setTableDesignerSearchQuery(bindNode.value);
1252
+ return;
1253
+ }
804
1254
 
805
- if (state.route.name === "data" && typeof state.dataBrowser.selectedRowIndex === "number") {
806
- event.preventDefault();
807
- clearDataRowSelection();
808
- return;
809
- }
1255
+ if (bindNode.dataset.bind === 'media-tagging-tag-search') {
1256
+ syncMediaTaggingTagSearchUi(bindNode);
1257
+ return;
1258
+ }
810
1259
 
811
- if (state.route.name === "editorResults" && typeof state.editor.selectedRowIndex === "number") {
812
- event.preventDefault();
813
- clearEditorRowSelection();
814
- }
815
- });
1260
+ if (bindNode.dataset.bind === 'table-designer-field') {
1261
+ if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
1262
+ return;
1263
+ }
816
1264
 
817
- document.addEventListener("input", (event) => {
818
- const bindNode = event.target.closest("[data-bind]");
1265
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
1266
+ return;
1267
+ }
819
1268
 
820
- if (!bindNode) {
821
- return;
822
- }
1269
+ if (bindNode.dataset.bind === 'table-designer-column-field') {
1270
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
1271
+ return;
1272
+ }
823
1273
 
824
- if (bindNode.dataset.bind === "current-query") {
825
- syncQueryEditorHighlight(bindNode);
826
- syncQueryEditorScroll(bindNode);
827
- setCurrentQuery(bindNode.value);
828
- return;
829
- }
1274
+ if (bindNode.dataset.bind === 'media-tagging-field') {
1275
+ if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
1276
+ return;
1277
+ }
830
1278
 
831
- if (bindNode.dataset.bind === "data-search-query") {
832
- setDataSearchQuery(bindNode.value);
833
- return;
834
- }
1279
+ // Skip select elements - they're handled in the change event with preview refresh
1280
+ if (bindNode instanceof HTMLSelectElement) {
1281
+ return;
1282
+ }
835
1283
 
836
- if (bindNode.dataset.bind === "table-designer-search") {
837
- setTableDesignerSearchQuery(bindNode.value);
838
- return;
839
- }
1284
+ if (bindNode instanceof HTMLTextAreaElement && bindNode.dataset.sqlHighlight === 'true') {
1285
+ syncQueryEditorHighlight(bindNode);
1286
+ syncQueryEditorScroll(bindNode);
1287
+ }
840
1288
 
841
- if (bindNode.dataset.bind === "table-designer-field") {
842
- if (bindNode instanceof HTMLInputElement && bindNode.type === "checkbox") {
843
- return;
1289
+ updateCurrentMediaTaggingField(bindNode.dataset.field, bindNode.value);
1290
+ return;
844
1291
  }
845
1292
 
846
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
847
- return;
848
- }
1293
+ if (bindNode.dataset.bind === 'media-tagging-tag-form-field') {
1294
+ if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
1295
+ return;
1296
+ }
849
1297
 
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
- }
1298
+ updateCurrentMediaTaggingTagFormField(bindNode.dataset.field, bindNode.value);
1299
+ return;
1300
+ }
858
1301
 
859
- if (bindNode.dataset.bind === "query-history-search") {
860
- setQueryHistorySearchInput(bindNode.value);
861
- return;
862
- }
1302
+ if (bindNode.dataset.bind === 'query-history-search') {
1303
+ setQueryHistorySearchInput(bindNode.value);
1304
+ return;
1305
+ }
863
1306
 
864
- if (bindNode.dataset.bind === "query-chart-draft:name") {
865
- updateCurrentQueryChartDraftField("name", bindNode.value);
866
- }
1307
+ if (bindNode.dataset.bind === 'query-chart-draft:name') {
1308
+ updateCurrentQueryChartDraftField('name', bindNode.value);
1309
+ }
867
1310
  });
868
1311
 
869
1312
  document.addEventListener(
870
- "scroll",
871
- (event) => {
872
- const target = event.target;
1313
+ 'scroll',
1314
+ event => {
1315
+ const target = event.target;
873
1316
 
874
- if (!(target instanceof HTMLTextAreaElement) || target.dataset.bind !== "current-query") {
875
- return;
876
- }
1317
+ if (!(target instanceof HTMLTextAreaElement)) {
1318
+ return;
1319
+ }
877
1320
 
878
- syncQueryEditorScroll(target);
879
- },
880
- true
1321
+ if (target.dataset.bind !== 'current-query' && target.dataset.sqlHighlight !== 'true') {
1322
+ return;
1323
+ }
1324
+
1325
+ syncQueryEditorScroll(target);
1326
+ },
1327
+ true,
881
1328
  );
882
1329
 
883
- document.addEventListener("change", (event) => {
884
- const bindNode = event.target.closest("[data-bind]");
1330
+ document.addEventListener('change', event => {
1331
+ const bindNode = event.target.closest('[data-bind]');
885
1332
 
886
- if (!bindNode) {
887
- return;
888
- }
1333
+ if (!bindNode) {
1334
+ return;
1335
+ }
889
1336
 
890
- if (bindNode.dataset.bind === "data-search-column") {
891
- setDataSearchColumn(bindNode.value);
892
- return;
893
- }
1337
+ if (bindNode.dataset.bind === 'data-search-column') {
1338
+ setDataSearchColumn(bindNode.value);
1339
+ return;
1340
+ }
894
1341
 
895
- if (bindNode.dataset.bind === "table-designer-import-file") {
896
- void handleTableDesignerCsvImport(bindNode);
897
- return;
898
- }
1342
+ if (bindNode.dataset.bind === 'media-tagging-field') {
1343
+ updateCurrentMediaTaggingField(bindNode.dataset.field, bindNode.value);
1344
+ void refreshMediaTaggingPreview();
1345
+ return;
1346
+ }
899
1347
 
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;
1348
+ if (bindNode.dataset.bind === 'media-tagging-tag-form-field') {
1349
+ const nextValue =
1350
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : bindNode.value;
1351
+ updateCurrentMediaTaggingTagFormField(bindNode.dataset.field, nextValue);
1352
+ return;
904
1353
  }
905
1354
 
906
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
907
- return;
908
- }
1355
+ if (bindNode.dataset.bind === 'media-tagging-tag-selection') {
1356
+ const nextValue =
1357
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : false;
1358
+ toggleCurrentMediaTagSelection(bindNode.dataset.tagKey, nextValue, { notify: false });
1359
+ syncMediaTagSelectionUi(bindNode, nextValue);
1360
+ return;
1361
+ }
909
1362
 
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);
947
- }
948
- });
1363
+ if (bindNode.dataset.bind === 'table-designer-import-file') {
1364
+ void handleTableDesignerCsvImport(bindNode);
1365
+ return;
1366
+ }
949
1367
 
950
- document.addEventListener("submit", async (event) => {
951
- const form = event.target.closest("[data-form]");
952
-
953
- if (!form) {
954
- return;
955
- }
956
-
957
- event.preventDefault();
958
- const formData = new FormData(form);
959
-
960
- switch (form.dataset.form) {
961
- case "open-connection": {
962
- resetStructureGraphForDatabaseChange();
963
- const connection = await submitOpenConnection({
964
- path: String(formData.get("path") ?? ""),
965
- label: String(formData.get("label") ?? ""),
966
- readOnly: formData.get("readOnly") === "on",
967
- });
968
-
969
- if (connection) {
970
- router.navigate("/overview");
971
- }
972
- return;
973
- }
974
- case "create-connection": {
975
- resetStructureGraphForDatabaseChange();
976
- const connection = await submitCreateConnection({
977
- path: String(formData.get("path") ?? ""),
978
- label: String(formData.get("label") ?? ""),
979
- });
980
-
981
- if (connection) {
982
- router.navigate("/overview");
983
- }
984
- return;
985
- }
986
- case "import-sql": {
987
- resetStructureGraphForDatabaseChange();
988
- const targetMode = String(formData.get("targetMode") ?? "active");
989
- const payload = {
990
- sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
991
- label: String(formData.get("label") ?? ""),
992
- };
993
-
994
- if (targetMode === "recent") {
995
- payload.targetConnectionId = String(formData.get("targetConnectionId") ?? "");
996
- } else if (targetMode === "create") {
997
- payload.createNew = true;
998
- payload.targetPath = String(formData.get("targetPath") ?? "");
999
- } else if (targetMode === "path") {
1000
- payload.targetPath = String(formData.get("targetPath") ?? "");
1001
- }
1002
-
1003
- const result = await submitImportSql(payload);
1004
- if (result) {
1005
- router.navigate("/overview");
1006
- }
1007
- return;
1008
- }
1009
- case "edit-connection": {
1010
- const connectionId = String(formData.get("connectionId") ?? "");
1011
- const isActiveConnection = getState().connections.active?.id === connectionId;
1012
- const logoFile = formData.get("logoFile");
1013
- const logoUpload = await buildConnectionLogoUpload(logoFile);
1014
-
1015
- if (isActiveConnection) {
1016
- resetStructureGraphForDatabaseChange();
1017
- }
1018
-
1019
- await submitEditConnection(connectionId, {
1020
- path: String(formData.get("path") ?? ""),
1021
- label: String(formData.get("label") ?? ""),
1022
- readOnly: formData.get("readOnly") === "on",
1023
- clearLogo: formData.get("clearLogo") === "on" && !logoUpload,
1024
- logoUpload,
1025
- });
1026
-
1027
- return;
1028
- }
1029
- case "delete-row-confirm":
1030
- await submitDeleteRowConfirmation();
1031
- return;
1032
- case "save-query-chart":
1033
- await saveCurrentQueryChartDraft();
1034
- return;
1035
- case "delete-query-chart":
1036
- await submitDeleteChartConfirmation();
1037
- return;
1038
- case "save-data-row": {
1039
- const values = {};
1040
-
1041
- for (const [key, value] of formData.entries()) {
1042
- if (!key.startsWith("field:")) {
1043
- continue;
1368
+ if (bindNode.dataset.bind === 'table-designer-field') {
1369
+ if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
1370
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
1371
+ return;
1044
1372
  }
1045
1373
 
1046
- values[key.slice("field:".length)] = String(value ?? "");
1047
- }
1374
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
1375
+ return;
1376
+ }
1377
+
1378
+ if (bindNode.dataset.bind === 'table-designer-column-field') {
1379
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
1380
+ return;
1381
+ }
1382
+
1383
+ if (bindNode.dataset.bind === 'table-designer-column-flag') {
1384
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.checked);
1385
+ return;
1386
+ }
1387
+
1388
+ if (bindNode.dataset.bind === 'query-chart-draft:chartType') {
1389
+ updateCurrentQueryChartDraftField('chartType', bindNode.value);
1390
+ return;
1391
+ }
1392
+
1393
+ if (bindNode.dataset.bind === 'query-chart-draft:tableVisible') {
1394
+ updateCurrentQueryChartDraftField('tableVisible', bindNode.checked);
1395
+ return;
1396
+ }
1397
+
1398
+ if (bindNode.dataset.bind?.startsWith('query-chart-draft-config:')) {
1399
+ const field = bindNode.dataset.bind.slice('query-chart-draft-config:'.length);
1400
+ const value =
1401
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox'
1402
+ ? bindNode.checked
1403
+ : bindNode.value === ''
1404
+ ? null
1405
+ : bindNode.value;
1406
+ updateCurrentQueryChartDraftConfigField(field, value);
1407
+ }
1408
+ });
1409
+
1410
+ document.addEventListener('submit', async event => {
1411
+ const form = event.target.closest('[data-form]');
1048
1412
 
1049
- await submitDataRowUpdate(
1050
- String(formData.get("rowIndex") ?? ""),
1051
- values
1052
- );
1053
- return;
1413
+ if (!form) {
1414
+ return;
1054
1415
  }
1055
- case "save-editor-row": {
1056
- const values = {};
1057
1416
 
1058
- for (const [key, value] of formData.entries()) {
1059
- if (!key.startsWith("field:")) {
1060
- continue;
1417
+ event.preventDefault();
1418
+ const formData = new FormData(form);
1419
+
1420
+ switch (form.dataset.form) {
1421
+ case 'open-connection': {
1422
+ resetStructureGraphForDatabaseChange();
1423
+ const connection = await submitOpenConnection({
1424
+ path: String(formData.get('path') ?? ''),
1425
+ label: String(formData.get('label') ?? ''),
1426
+ readOnly: formData.get('readOnly') === 'on',
1427
+ });
1428
+
1429
+ if (connection) {
1430
+ router.navigate('/overview');
1431
+ }
1432
+ return;
1433
+ }
1434
+ case 'create-connection': {
1435
+ resetStructureGraphForDatabaseChange();
1436
+ const connection = await submitCreateConnection({
1437
+ path: String(formData.get('path') ?? ''),
1438
+ label: String(formData.get('label') ?? ''),
1439
+ });
1440
+
1441
+ if (connection) {
1442
+ router.navigate('/overview');
1443
+ }
1444
+ return;
1445
+ }
1446
+ case 'import-sql': {
1447
+ resetStructureGraphForDatabaseChange();
1448
+ const targetMode = String(formData.get('targetMode') ?? 'active');
1449
+ const payload = {
1450
+ sqlFilePath: String(formData.get('sqlFilePath') ?? ''),
1451
+ label: String(formData.get('label') ?? ''),
1452
+ };
1453
+
1454
+ if (targetMode === 'recent') {
1455
+ payload.targetConnectionId = String(formData.get('targetConnectionId') ?? '');
1456
+ } else if (targetMode === 'create') {
1457
+ payload.createNew = true;
1458
+ payload.targetPath = String(formData.get('targetPath') ?? '');
1459
+ } else if (targetMode === 'path') {
1460
+ payload.targetPath = String(formData.get('targetPath') ?? '');
1461
+ }
1462
+
1463
+ const result = await submitImportSql(payload);
1464
+ if (result) {
1465
+ router.navigate('/overview');
1466
+ }
1467
+ return;
1061
1468
  }
1469
+ case 'edit-connection': {
1470
+ const connectionId = String(formData.get('connectionId') ?? '');
1471
+ const isActiveConnection = getState().connections.active?.id === connectionId;
1472
+ const logoFile = formData.get('logoFile');
1473
+ const logoUpload = await buildConnectionLogoUpload(logoFile);
1474
+
1475
+ if (isActiveConnection) {
1476
+ resetStructureGraphForDatabaseChange();
1477
+ }
1478
+
1479
+ await submitEditConnection(connectionId, {
1480
+ path: String(formData.get('path') ?? ''),
1481
+ label: String(formData.get('label') ?? ''),
1482
+ readOnly: formData.get('readOnly') === 'on',
1483
+ clearLogo: formData.get('clearLogo') === 'on' && !logoUpload,
1484
+ logoUpload,
1485
+ });
1486
+
1487
+ return;
1488
+ }
1489
+ case 'delete-row-confirm':
1490
+ await submitDeleteRowConfirmation();
1491
+ return;
1492
+ case 'save-query-chart':
1493
+ await saveCurrentQueryChartDraft();
1494
+ return;
1495
+ case 'delete-query-chart':
1496
+ await submitDeleteChartConfirmation();
1497
+ return;
1498
+ case 'delete-query-history-confirm':
1499
+ await submitDeleteQueryHistoryConfirmation();
1500
+ return;
1501
+ case 'create-media-tagging-tag-table':
1502
+ await submitCreateMediaTaggingTagTable();
1503
+ return;
1504
+ case 'create-media-tagging-mapping-table':
1505
+ await submitCreateMediaTaggingMappingTable();
1506
+ return;
1507
+ case 'save-data-row': {
1508
+ const values = {};
1509
+
1510
+ for (const [key, value] of formData.entries()) {
1511
+ if (!key.startsWith('field:')) {
1512
+ continue;
1513
+ }
1514
+
1515
+ values[key.slice('field:'.length)] = String(value ?? '');
1516
+ }
1517
+
1518
+ let rowIdentity = null;
1519
+ const rawRowIdentity = String(formData.get('rowIdentity') ?? '').trim();
1520
+
1521
+ if (rawRowIdentity) {
1522
+ try {
1523
+ rowIdentity = JSON.parse(rawRowIdentity);
1524
+ } catch (error) {
1525
+ rowIdentity = null;
1526
+ }
1527
+ }
1528
+
1529
+ await submitDataRowUpdate(String(formData.get('rowIndex') ?? ''), values, rowIdentity);
1530
+ return;
1531
+ }
1532
+ case 'save-editor-row': {
1533
+ const values = {};
1534
+
1535
+ for (const [key, value] of formData.entries()) {
1536
+ if (!key.startsWith('field:')) {
1537
+ continue;
1538
+ }
1539
+
1540
+ values[key.slice('field:'.length)] = String(value ?? '');
1541
+ }
1062
1542
 
1063
- values[key.slice("field:".length)] = String(value ?? "");
1064
- }
1065
-
1066
- await submitEditorRowUpdate(
1067
- String(formData.get("rowIndex") ?? ""),
1068
- values
1069
- );
1070
- return;
1071
- }
1072
- case "save-query-history-title":
1073
- await saveQueryHistoryTitle(
1074
- String(formData.get("historyId") ?? ""),
1075
- String(formData.get("title") ?? "")
1076
- );
1077
- return;
1078
- case "save-query-history-notes":
1079
- await saveQueryHistoryNotes(
1080
- String(formData.get("historyId") ?? ""),
1081
- String(formData.get("notes") ?? "")
1082
- );
1083
- return;
1084
- default:
1085
- }
1543
+ await submitEditorRowUpdate(String(formData.get('rowIndex') ?? ''), values);
1544
+ return;
1545
+ }
1546
+ case 'save-query-history-title': {
1547
+ const historyId = String(formData.get('historyId') ?? '');
1548
+ const updatedItem = await saveQueryHistoryTitle(historyId, String(formData.get('title') ?? ''));
1549
+
1550
+ if (updatedItem) {
1551
+ syncQueryHistoryUi(historyId);
1552
+ }
1553
+ return;
1554
+ }
1555
+ case 'save-query-history-notes': {
1556
+ const historyId = String(formData.get('historyId') ?? '');
1557
+ const updatedItem = await saveQueryHistoryNotes(historyId, String(formData.get('notes') ?? ''));
1558
+
1559
+ if (updatedItem) {
1560
+ syncQueryHistoryUi(historyId);
1561
+ }
1562
+ return;
1563
+ }
1564
+ default:
1565
+ }
1086
1566
  });
1087
1567
 
1088
1568
  subscribe(renderApp);
1089
1569
  renderApp(getState());
1090
1570
  initializeApp().then(() => {
1091
- router.start();
1571
+ router.start();
1092
1572
  });