sqlite-hub 0.8.7 → 0.9.1

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