sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -24,6 +24,8 @@ import { renderTopNav } from './components/topNav.js';
24
24
  import { createRouter } from './router.js';
25
25
  import {
26
26
  createActiveConnectionBackup,
27
+ createDocument,
28
+ createDocumentFromMarkdownExport,
27
29
  clearCurrentQuery,
28
30
  clearDataRowSelection,
29
31
  clearEditorRowSelection,
@@ -48,11 +50,16 @@ import {
48
50
  openDeleteQueryHistoryModal,
49
51
  openDeleteQueryChartModal,
50
52
  openDataExportModal,
53
+ openDeleteDocumentModal,
51
54
  openQueryExportModal,
52
55
  openDataRowByIdentity,
53
56
  openEditConnectionModal,
54
57
  openCreateQueryChartModal,
58
+ openCopyColumnModal,
55
59
  openEditQueryChartModal,
60
+ openDocumentInsertNoteModal,
61
+ openDocumentInsertTableModal,
62
+ preserveCurrentDataRowSelectionForReload,
56
63
  openDataRowUpdatePreview,
57
64
  openEditorRowUpdatePreview,
58
65
  refreshCurrentRoute,
@@ -64,6 +71,7 @@ import {
64
71
  runQueryHistoryItem,
65
72
  skipCurrentMediaTaggingItem,
66
73
  saveCurrentQueryChartDraft,
74
+ saveCurrentDocument,
67
75
  saveCurrentMediaTaggingConfig,
68
76
  selectDataRow,
69
77
  selectEditorRow,
@@ -88,6 +96,9 @@ import {
88
96
  setEditorPanelVisibility,
89
97
  setEditorTab,
90
98
  submitDeleteChartConfirmation,
99
+ submitDeleteDocumentConfirmation,
100
+ submitDocumentInsertNote,
101
+ submitDocumentInsertTable,
91
102
  submitCreateMediaTaggingTagTable,
92
103
  submitCreateMediaTaggingMappingTable,
93
104
  submitDeleteQueryHistoryConfirmation,
@@ -97,16 +108,22 @@ import {
97
108
  sortEditorResultsByColumn,
98
109
  setQueryHistorySearchInput,
99
110
  setQueryHistoryTab,
111
+ setCopyColumnModalError,
112
+ setCopyColumnModalEditedText,
113
+ setCopyColumnModalSubmitting,
100
114
  setRoute,
101
115
  saveQueryHistoryNotes,
102
116
  saveQueryHistoryTitle,
103
117
  saveCurrentTableDesignerDraft,
104
118
  toggleChartsResultsPanel,
105
119
  toggleChartsSqlPanel,
120
+ toggleCurrentDocumentTodo,
121
+ toggleDocumentsPane,
106
122
  setMediaTaggingWorkflowMediaDetailsVisible,
107
123
  setMediaTaggingWorkflowMediaRotationDegrees,
108
124
  queueTableDesignerCsvImport,
109
125
  showToast,
126
+ storeCopyColumnPreferences,
110
127
  submitCreateConnection,
111
128
  createCurrentMediaTag,
112
129
  submitDeleteRowConfirmation,
@@ -118,9 +135,13 @@ import {
118
135
  toggleQueryHistorySavedState,
119
136
  updateCurrentMediaTaggingField,
120
137
  updateCurrentMediaTaggingTagFormField,
138
+ updateCopyColumnModalFormatField,
139
+ updateCurrentDocumentDraftField,
140
+ updateDocumentInsertQuerySelection,
121
141
  updateCurrentQueryChartDraftConfigField,
122
142
  updateCurrentQueryChartDraftField,
123
143
  updateCurrentTableDesignerColumnField,
144
+ updateCurrentTableDesignerConstraintField,
124
145
  updateCurrentTableDesignerField,
125
146
  addCurrentTableDesignerColumn,
126
147
  applyCurrentMediaTaggingSelection,
@@ -129,6 +150,7 @@ import {
129
150
  import { renderChartsDetail, renderChartsView } from './views/charts.js';
130
151
  import { renderConnectionsView } from './views/connections.js';
131
152
  import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
153
+ import { renderDocumentsView } from './views/documents.js';
132
154
  import { renderEditorView } from './views/editor.js';
133
155
  import { renderLandingView } from './views/landing.js';
134
156
  import { renderMediaTaggingView } from './views/mediaTagging.js';
@@ -137,7 +159,29 @@ import { renderSettingsView } from './views/settings.js';
137
159
  import { renderStructureView } from './views/structure.js';
138
160
  import { renderTableDesignerView } from './views/tableDesigner.js';
139
161
  import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
140
- import { highlightSql } from './utils/format.js';
162
+ import {
163
+ buildCopyColumnText,
164
+ getCopyColumnExportMetadata,
165
+ isMarkdownTodoCopyColumnMode,
166
+ normalizeCopyColumnMode,
167
+ } from './utils/copyColumnExport.js';
168
+ import { formatNumber, highlightSql } from './utils/format.js';
169
+ import {
170
+ compactPathForDisplay,
171
+ detectFilePathValue,
172
+ getPathTypeLabel,
173
+ } from './utils/filePathPreview.js';
174
+ import { formatSqlQuery } from './utils/sqlFormatter.js';
175
+ import {
176
+ buildDataRowEditorJsonObject,
177
+ buildEditorRowEditorJsonObject,
178
+ stringifyRowEditorJson,
179
+ } from './utils/rowEditorJson.js';
180
+ import { getTimestampPreviewForField } from './utils/timestampPreview.js';
181
+ import {
182
+ formatTextCellCharacterCount,
183
+ getTextCellCharacterCount,
184
+ } from './utils/textCellStats.js';
141
185
 
142
186
  const appRoot = document.querySelector('#app');
143
187
 
@@ -168,6 +212,10 @@ let lastRenderedLockedRoute = false;
168
212
  let pendingNewTableDesignerAutofocus = false;
169
213
  let pendingQueryEditorFocus = false;
170
214
  let pendingMediaTaggingTagSearchFocus = false;
215
+ let documentAutosaveTimer = null;
216
+ let pendingDocumentAutosaveId = null;
217
+
218
+ const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
171
219
 
172
220
  const APP_TITLE = 'SQLite Hub';
173
221
  const ROUTE_TITLE_SEGMENTS = {
@@ -178,6 +226,7 @@ const ROUTE_TITLE_SEGMENTS = {
178
226
  editor: 'SQL Editor',
179
227
  editorResults: 'SQL Editor',
180
228
  charts: 'Charts',
229
+ documents: 'Documents',
181
230
  tableDesigner: 'Table Designer',
182
231
  mediaTaggingSetup: 'Media Tagging',
183
232
  mediaTaggingQueue: 'Tagging Queue',
@@ -768,13 +817,58 @@ function readFileAsText(file) {
768
817
  resolve(String(reader.result ?? ''));
769
818
  };
770
819
  reader.onerror = () => {
771
- reject(reader.error ?? new Error('The selected CSV file could not be read.'));
820
+ reject(reader.error ?? new Error('The selected file could not be read.'));
772
821
  };
773
822
 
774
823
  reader.readAsText(file);
775
824
  });
776
825
  }
777
826
 
827
+ function clearDocumentAutosaveTimer() {
828
+ if (documentAutosaveTimer !== null) {
829
+ window.clearTimeout(documentAutosaveTimer);
830
+ }
831
+
832
+ documentAutosaveTimer = null;
833
+ pendingDocumentAutosaveId = null;
834
+ }
835
+
836
+ function scheduleDocumentAutosave(documentId) {
837
+ clearDocumentAutosaveTimer();
838
+
839
+ if (!documentId) {
840
+ return;
841
+ }
842
+
843
+ pendingDocumentAutosaveId = String(documentId);
844
+ documentAutosaveTimer = window.setTimeout(async () => {
845
+ const state = getState();
846
+ const scheduledDocumentId = pendingDocumentAutosaveId;
847
+
848
+ documentAutosaveTimer = null;
849
+ pendingDocumentAutosaveId = null;
850
+
851
+ if (
852
+ state.route.name !== 'documents' ||
853
+ String(state.documents.selectedId ?? '') !== scheduledDocumentId ||
854
+ !state.documents.dirty
855
+ ) {
856
+ return;
857
+ }
858
+
859
+ if (state.documents.saving) {
860
+ scheduleDocumentAutosave(scheduledDocumentId);
861
+ return;
862
+ }
863
+
864
+ if (state.documents.deleting) {
865
+ return;
866
+ }
867
+
868
+ await saveCurrentDocument({ toast: false });
869
+ }, DOCUMENT_AUTOSAVE_DELAY_MS);
870
+ }
871
+
778
872
  async function buildConnectionLogoUpload(file) {
779
873
  if (!(file instanceof File) || !file.size) {
780
874
  return null;
@@ -823,6 +917,8 @@ function resolveView(state) {
823
917
  return renderOverviewView(state);
824
918
  case 'charts':
825
919
  return renderChartsView(state);
920
+ case 'documents':
921
+ return renderDocumentsView(state);
826
922
  case 'data':
827
923
  return renderDataView(state);
828
924
  case 'editor':
@@ -1072,6 +1168,39 @@ async function handleTableDesignerCsvImport(fileInput) {
1072
1168
  }
1073
1169
  }
1074
1170
 
1171
+ async function handleDocumentMarkdownImport(fileInput) {
1172
+ if (!(fileInput instanceof HTMLInputElement)) {
1173
+ return;
1174
+ }
1175
+
1176
+ const file = fileInput.files?.[0];
1177
+
1178
+ if (!(file instanceof File)) {
1179
+ return;
1180
+ }
1181
+
1182
+ try {
1183
+ const content = await readFileAsText(file);
1184
+ const document = await createDocumentFromMarkdownExport({
1185
+ filename: file.name,
1186
+ content,
1187
+ });
1188
+
1189
+ if (!document?.id) {
1190
+ showToast('Markdown document could not be imported.', 'alert');
1191
+ return;
1192
+ }
1193
+
1194
+ clearDocumentAutosaveTimer();
1195
+ showToast(`Document "${document.filename}" imported.`, 'success');
1196
+ router.navigate(`/documents/${encodeURIComponent(document.id)}`);
1197
+ } catch (error) {
1198
+ showToast(error?.message || 'Markdown import failed.', 'alert');
1199
+ } finally {
1200
+ fileInput.value = '';
1201
+ }
1202
+ }
1203
+
1075
1204
  function renderApp(state) {
1076
1205
  syncDocumentTitle(state);
1077
1206
 
@@ -1089,6 +1218,7 @@ function renderApp(state) {
1089
1218
  'editorResults',
1090
1219
  'data',
1091
1220
  'charts',
1221
+ 'documents',
1092
1222
  'structure',
1093
1223
  'tableDesigner',
1094
1224
  'mediaTaggingSetup',
@@ -1254,6 +1384,39 @@ async function executeEditorQueryAndNavigate() {
1254
1384
  router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
1255
1385
  }
1256
1386
 
1387
+ function formatCurrentQuery() {
1388
+ const currentQuery = getState().editor.sqlText ?? '';
1389
+ const formattedQuery = formatSqlQuery(currentQuery);
1390
+
1391
+ if (!formattedQuery) {
1392
+ showToast('No SQL query to format.', 'alert');
1393
+ return;
1394
+ }
1395
+
1396
+ if (formattedQuery === currentQuery) {
1397
+ focusQueryEditorInput();
1398
+ showToast('SQL query is already formatted.', 'muted');
1399
+ return;
1400
+ }
1401
+
1402
+ invalidateMainRenderCache();
1403
+ setCurrentQuery(formattedQuery);
1404
+
1405
+ const input = document.querySelector('[data-bind="current-query"]');
1406
+
1407
+ if (input instanceof HTMLTextAreaElement) {
1408
+ input.value = formattedQuery;
1409
+ syncQueryEditorHighlight(input);
1410
+ syncQueryEditorScroll(input);
1411
+ input.focus({ preventScroll: true });
1412
+ input.setSelectionRange(input.value.length, input.value.length);
1413
+ } else {
1414
+ pendingQueryEditorFocus = true;
1415
+ }
1416
+
1417
+ showToast('SQL query formatted.', 'success');
1418
+ }
1419
+
1257
1420
  const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
1258
1421
 
1259
1422
  function getOpenableUrl(value) {
@@ -1291,6 +1454,561 @@ function openRowEditorUrl(actionNode) {
1291
1454
  link.remove();
1292
1455
  }
1293
1456
 
1457
+ function getExportFilenameFromAction(actionNode) {
1458
+ const input = actionNode.closest('[data-export-modal]')?.querySelector('input[name="filename"]');
1459
+ return input instanceof HTMLInputElement ? input.value : '';
1460
+ }
1461
+
1462
+ function syncRowEditorTimestampPreview(inputNode) {
1463
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1464
+ const previewNode = fieldNode?.querySelector('[data-row-editor-timestamp-preview]');
1465
+
1466
+ if (!fieldNode || !previewNode) {
1467
+ return;
1468
+ }
1469
+
1470
+ const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
1471
+ const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
1472
+ const tableMeta = {
1473
+ columns: [
1474
+ {
1475
+ name: columnName,
1476
+ primaryKeyPosition: protectedKeyColumn ? 1 : 0,
1477
+ foreignKey: protectedKeyColumn,
1478
+ },
1479
+ ],
1480
+ foreignKeys: [],
1481
+ };
1482
+ const preview = getTimestampPreviewForField({
1483
+ columnName,
1484
+ value: inputNode.value,
1485
+ tableMeta,
1486
+ });
1487
+
1488
+ if (preview.kind !== 'timestamp') {
1489
+ previewNode.hidden = true;
1490
+ previewNode.textContent = '';
1491
+ return;
1492
+ }
1493
+
1494
+ previewNode.hidden = false;
1495
+ previewNode.textContent = `Interpretiert als Datum: ${preview.formatted}`;
1496
+ }
1497
+
1498
+ function syncRowEditorCharacterCount(inputNode) {
1499
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1500
+ const countNode = fieldNode?.querySelector('[data-row-editor-char-count]');
1501
+
1502
+ if (!fieldNode || !countNode) {
1503
+ return;
1504
+ }
1505
+
1506
+ const count = getTextCellCharacterCount(inputNode.value);
1507
+
1508
+ if (count === null) {
1509
+ countNode.hidden = true;
1510
+ countNode.textContent = '';
1511
+ return;
1512
+ }
1513
+
1514
+ countNode.hidden = false;
1515
+ countNode.textContent = formatTextCellCharacterCount(count);
1516
+ }
1517
+
1518
+ function syncRowEditorFilePathPreview(inputNode) {
1519
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1520
+ const previewNode = fieldNode?.querySelector('[data-row-editor-filepath-preview]');
1521
+
1522
+ if (!fieldNode || !previewNode) {
1523
+ return;
1524
+ }
1525
+
1526
+ const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
1527
+ const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
1528
+ const tableMeta = {
1529
+ columns: [
1530
+ {
1531
+ name: columnName,
1532
+ primaryKeyPosition: protectedKeyColumn ? 1 : 0,
1533
+ foreignKey: protectedKeyColumn,
1534
+ },
1535
+ ],
1536
+ foreignKeys: [],
1537
+ };
1538
+ const preview = detectFilePathValue(inputNode.value, columnName, tableMeta);
1539
+
1540
+ if (!preview) {
1541
+ previewNode.hidden = true;
1542
+ return;
1543
+ }
1544
+
1545
+ const filenameNode = previewNode.querySelector('[data-row-editor-filepath-filename]');
1546
+ const directoryRowNode = previewNode.querySelector('[data-row-editor-filepath-directory-row]');
1547
+ const directoryNode = previewNode.querySelector('[data-row-editor-filepath-directory]');
1548
+ const extensionNode = previewNode.querySelector('[data-row-editor-filepath-extension]');
1549
+ const typeNode = previewNode.querySelector('[data-row-editor-filepath-type]');
1550
+
1551
+ if (filenameNode) {
1552
+ filenameNode.textContent = preview.fileName ?? 'N/A';
1553
+ }
1554
+
1555
+ if (directoryRowNode) {
1556
+ directoryRowNode.hidden = !preview.directory;
1557
+ }
1558
+
1559
+ if (directoryNode) {
1560
+ directoryNode.textContent = preview.directory ? compactPathForDisplay(preview.directory, 72) : '';
1561
+ directoryNode.setAttribute('title', preview.directory ?? '');
1562
+ }
1563
+
1564
+ if (extensionNode) {
1565
+ extensionNode.textContent = preview.extension ?? 'N/A';
1566
+ }
1567
+
1568
+ if (typeNode) {
1569
+ typeNode.textContent = getPathTypeLabel(preview.pathType);
1570
+ }
1571
+
1572
+ previewNode.hidden = false;
1573
+ }
1574
+
1575
+ function closeCopyColumnMenus(exceptMenu = null) {
1576
+ document.querySelectorAll('[data-copy-column-menu][open]').forEach(menu => {
1577
+ if (menu !== exceptMenu && menu instanceof HTMLDetailsElement) {
1578
+ menu.open = false;
1579
+ }
1580
+ });
1581
+ }
1582
+
1583
+ function closeSidebarDatabasePickers(exceptPicker = null) {
1584
+ document.querySelectorAll('.sidebar-db-picker[open]').forEach(picker => {
1585
+ if (picker !== exceptPicker && picker instanceof HTMLDetailsElement) {
1586
+ picker.open = false;
1587
+ }
1588
+ });
1589
+ }
1590
+
1591
+ function openCopyColumnMenu(headerNode) {
1592
+ const menu = headerNode?.querySelector('[data-copy-column-menu]');
1593
+
1594
+ if (!(menu instanceof HTMLDetailsElement)) {
1595
+ return;
1596
+ }
1597
+
1598
+ closeCopyColumnMenus(menu);
1599
+ menu.open = true;
1600
+ menu.querySelector('summary')?.focus({ preventScroll: true });
1601
+ }
1602
+
1603
+ function getCopyColumnResult(state, scope) {
1604
+ return scope === 'charts' ? state.charts.result : state.editor.result;
1605
+ }
1606
+
1607
+ function slugifyExportFilenamePart(value, fallback = 'column') {
1608
+ const slug = String(value ?? '')
1609
+ .trim()
1610
+ .toLowerCase()
1611
+ .replace(/[^a-z0-9]+/g, '-')
1612
+ .replace(/^-+|-+$/g, '');
1613
+
1614
+ return slug || fallback;
1615
+ }
1616
+
1617
+ function buildCopyColumnExportFilename(columnName, copyMode) {
1618
+ const metadata = getCopyColumnExportMetadata(copyMode);
1619
+ const columnSlug = slugifyExportFilenamePart(columnName);
1620
+
1621
+ return `${columnSlug}-${metadata.suffix}.${metadata.extension}`;
1622
+ }
1623
+
1624
+ function normalizeMarkdownDownloadFilename(value) {
1625
+ let filename = String(value ?? '')
1626
+ .trim()
1627
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
1628
+ .replace(/[\\/]+/g, ' ')
1629
+ .replace(/\s+/g, ' ')
1630
+ .replace(/^\.+/, '')
1631
+ .trim();
1632
+
1633
+ if (!filename) {
1634
+ filename = 'document.md';
1635
+ }
1636
+
1637
+ if (!/\.md$/i.test(filename)) {
1638
+ filename = `${filename}.md`;
1639
+ }
1640
+
1641
+ return filename;
1642
+ }
1643
+
1644
+ function countEditedMarkdownTodoItems(text) {
1645
+ const lines = String(text ?? '')
1646
+ .split(/\r\n|\r|\n/g)
1647
+ .filter(line => line.trim());
1648
+
1649
+ return lines.length;
1650
+ }
1651
+
1652
+ function buildDocumentTimestampSlug(date = new Date()) {
1653
+ const pad = value => String(value).padStart(2, '0');
1654
+
1655
+ return [
1656
+ date.getFullYear(),
1657
+ pad(date.getMonth() + 1),
1658
+ pad(date.getDate()),
1659
+ pad(date.getHours()),
1660
+ pad(date.getMinutes()),
1661
+ ].join('-');
1662
+ }
1663
+
1664
+ function buildCopyColumnDocumentFilename(columnName) {
1665
+ const columnSlug = slugifyExportFilenamePart(columnName);
1666
+
1667
+ return `${columnSlug}-todos-${buildDocumentTimestampSlug()}.md`;
1668
+ }
1669
+
1670
+ function buildCopyColumnDocumentContent({ state, columnName, text, valueCount }) {
1671
+ const activeConnection = state.connections.active;
1672
+ const lines = [
1673
+ `# ${columnName || 'Column'} todos`,
1674
+ '',
1675
+ `- Database: ${activeConnection?.label || 'Active database'}`,
1676
+ `- Column: ${columnName || 'N/A'}`,
1677
+ `- Items: ${valueCount}`,
1678
+ `- Generated: ${new Date().toLocaleString()}`,
1679
+ '',
1680
+ ];
1681
+
1682
+ lines.push('## Todos', '', String(text ?? '').trim(), '');
1683
+
1684
+ return lines.join('\n');
1685
+ }
1686
+
1687
+ function downloadTextFile({ text, filename, mimeType }) {
1688
+ const blob = new Blob([text], { type: mimeType });
1689
+ const url = URL.createObjectURL(blob);
1690
+ const link = document.createElement('a');
1691
+
1692
+ link.href = url;
1693
+ link.download = filename;
1694
+ document.body.appendChild(link);
1695
+ link.click();
1696
+ link.remove();
1697
+ URL.revokeObjectURL(url);
1698
+ }
1699
+
1700
+ function getSelectedDataBrowserRowForJson(state) {
1701
+ if (state.dataBrowser.selectedRow) {
1702
+ return state.dataBrowser.selectedRow;
1703
+ }
1704
+
1705
+ const rowIndex = state.dataBrowser.selectedRowIndex;
1706
+
1707
+ if (typeof rowIndex !== 'number') {
1708
+ return null;
1709
+ }
1710
+
1711
+ return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
1712
+ }
1713
+
1714
+ function getSelectedEditorResultRowForJson(state) {
1715
+ const rowIndex = state.editor.selectedRowIndex;
1716
+
1717
+ if (typeof rowIndex !== 'number') {
1718
+ return null;
1719
+ }
1720
+
1721
+ return state.editor.result?.rows?.[rowIndex] ?? null;
1722
+ }
1723
+
1724
+ function applyChangedRowEditorFormValues(rowObject) {
1725
+ const form = shellRefs.panel.querySelector(
1726
+ 'form[data-form="save-data-row"], form[data-form="save-editor-row"]',
1727
+ );
1728
+
1729
+ if (!form) {
1730
+ return rowObject;
1731
+ }
1732
+
1733
+ const nextRowObject = { ...rowObject };
1734
+ const formData = new FormData(form);
1735
+
1736
+ for (const [key, value] of formData.entries()) {
1737
+ if (!key.startsWith('field:')) {
1738
+ continue;
1739
+ }
1740
+
1741
+ const control = Array.from(form.elements).find(element => element.name === key);
1742
+ const initialValue = control?.dataset?.rowEditorInitialValue;
1743
+ const currentValue = String(value ?? '');
1744
+
1745
+ if (initialValue !== undefined && currentValue === initialValue) {
1746
+ continue;
1747
+ }
1748
+
1749
+ nextRowObject[key.slice('field:'.length)] = currentValue;
1750
+ }
1751
+
1752
+ return nextRowObject;
1753
+ }
1754
+
1755
+ function buildRowEditorJsonPayload(state) {
1756
+ if (state.route.name === 'data') {
1757
+ const row = getSelectedDataBrowserRowForJson(state);
1758
+ const table = state.dataBrowser.table;
1759
+ const tableName = table?.name ?? state.dataBrowser.selectedTable ?? 'row';
1760
+
1761
+ if (!row || !table) {
1762
+ return null;
1763
+ }
1764
+
1765
+ const rowObject = applyChangedRowEditorFormValues(
1766
+ buildDataRowEditorJsonObject({
1767
+ row,
1768
+ columns: table.columns ?? table.columnMeta ?? [],
1769
+ }),
1770
+ );
1771
+
1772
+ return {
1773
+ filename: `${slugifyExportFilenamePart(tableName, 'row')}-row.json`,
1774
+ label: tableName,
1775
+ text: stringifyRowEditorJson(rowObject),
1776
+ };
1777
+ }
1778
+
1779
+ if (state.route.name === 'editorResults') {
1780
+ const row = getSelectedEditorResultRowForJson(state);
1781
+ const result = state.editor.result;
1782
+ const rowIndex = state.editor.selectedRowIndex;
1783
+ const tableName = result?.editing?.tableName ?? 'query-result';
1784
+
1785
+ if (!row || !result) {
1786
+ return null;
1787
+ }
1788
+
1789
+ const rowObject = applyChangedRowEditorFormValues(
1790
+ buildEditorRowEditorJsonObject({
1791
+ row,
1792
+ editingColumns: result.editing?.columns ?? [],
1793
+ resultColumns: result.columns ?? [],
1794
+ }),
1795
+ );
1796
+
1797
+ return {
1798
+ filename: `${slugifyExportFilenamePart(tableName, 'query-result')}-row-${Number(rowIndex) + 1}.json`,
1799
+ label: tableName,
1800
+ text: stringifyRowEditorJson(rowObject),
1801
+ };
1802
+ }
1803
+
1804
+ return null;
1805
+ }
1806
+
1807
+ async function copyRowEditorJson() {
1808
+ const payload = buildRowEditorJsonPayload(getState());
1809
+
1810
+ if (!payload) {
1811
+ showToast('No row is selected.', 'alert');
1812
+ return;
1813
+ }
1814
+
1815
+ if (!navigator.clipboard?.writeText) {
1816
+ showToast('Clipboard API is not available.', 'alert');
1817
+ return;
1818
+ }
1819
+
1820
+ try {
1821
+ await navigator.clipboard.writeText(payload.text);
1822
+ showToast(`Row from "${payload.label}" copied as JSON.`, 'success');
1823
+ } catch (error) {
1824
+ showToast('Row JSON could not be copied.', 'alert');
1825
+ }
1826
+ }
1827
+
1828
+ function exportRowEditorJson() {
1829
+ const payload = buildRowEditorJsonPayload(getState());
1830
+
1831
+ if (!payload) {
1832
+ showToast('No row is selected.', 'alert');
1833
+ return;
1834
+ }
1835
+
1836
+ downloadTextFile({
1837
+ text: payload.text,
1838
+ filename: payload.filename,
1839
+ mimeType: 'application/json;charset=utf-8',
1840
+ });
1841
+ showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
1842
+ }
1843
+
1844
+ function exportCurrentDocumentMarkdown() {
1845
+ const documents = getState().documents;
1846
+
1847
+ if (!documents.selectedId) {
1848
+ showToast('No document is selected.', 'alert');
1849
+ return;
1850
+ }
1851
+
1852
+ const filename = normalizeMarkdownDownloadFilename(documents.draftFilename || documents.selected?.filename);
1853
+
1854
+ downloadTextFile({
1855
+ text: documents.draftContent ?? '',
1856
+ filename,
1857
+ mimeType: 'text/markdown;charset=utf-8',
1858
+ });
1859
+ showToast(`Document "${filename}" exported.`, 'success');
1860
+ }
1861
+
1862
+ function getCurrentDocumentEditorInsertionRange() {
1863
+ const textarea = document.querySelector('.documents-editor-input');
1864
+
1865
+ if (!(textarea instanceof HTMLTextAreaElement)) {
1866
+ return null;
1867
+ }
1868
+
1869
+ return {
1870
+ start: textarea.selectionStart,
1871
+ end: textarea.selectionEnd,
1872
+ };
1873
+ }
1874
+
1875
+ function preserveDataRowEditorSelectionForReload() {
1876
+ preserveCurrentDataRowSelectionForReload();
1877
+ }
1878
+
1879
+ async function submitCopyColumnModal(formData) {
1880
+ const state = getState();
1881
+ const modal = state.modal;
1882
+
1883
+ if (modal?.kind !== 'copy-column') {
1884
+ return;
1885
+ }
1886
+
1887
+ const scope = String(formData.get('scope') ?? modal.scope ?? 'editor');
1888
+ const columnName = String(formData.get('columnName') ?? modal.columnName ?? '');
1889
+ const copyMode = normalizeCopyColumnMode(String(formData.get('copyMode') ?? modal.copyMode ?? 'column'));
1890
+ const separator = String(formData.get('separator') ?? ',');
1891
+ const wrapper = String(formData.get('wrapper') ?? '');
1892
+ const lineBreaks = formData.get('lineBreaks') === 'on';
1893
+ const outputSeparator = lineBreaks ? '\n' : separator;
1894
+ const intent = String(formData.get('intent') ?? 'copy');
1895
+ const isExportIntent = intent === 'export';
1896
+ const isDocumentIntent = intent === 'document';
1897
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(copyMode);
1898
+ const editedText = formData.has('editedText') ? String(formData.get('editedText') ?? '') : null;
1899
+ const result = getCopyColumnResult(state, scope);
1900
+ const hasColumn = (result?.columns ?? []).some(column => String(column) === columnName);
1901
+
1902
+ if (!hasColumn) {
1903
+ setCopyColumnModalError({
1904
+ code: 'COPY_COLUMN_UNAVAILABLE',
1905
+ message: 'Column is no longer available in the current result set.',
1906
+ });
1907
+ showToast('Column could not be copied.', 'alert');
1908
+ return;
1909
+ }
1910
+
1911
+ if (!isMarkdownTodo) {
1912
+ storeCopyColumnPreferences({ separator, wrapper, lineBreaks });
1913
+ } else if (editedText !== null) {
1914
+ setCopyColumnModalEditedText(editedText);
1915
+ }
1916
+ setCopyColumnModalSubmitting(true);
1917
+
1918
+ const generatedOutput = buildCopyColumnText({
1919
+ result,
1920
+ columnName,
1921
+ copyMode,
1922
+ separator: outputSeparator,
1923
+ wrapper,
1924
+ });
1925
+ const text = isMarkdownTodo && editedText !== null ? editedText : generatedOutput.text;
1926
+ const valueCount = isMarkdownTodo && editedText !== null
1927
+ ? countEditedMarkdownTodoItems(editedText)
1928
+ : generatedOutput.valueCount;
1929
+
1930
+ try {
1931
+ if (isDocumentIntent) {
1932
+ if (!isMarkdownTodo) {
1933
+ throw new Error('Document export is only available for Markdown Todo columns.');
1934
+ }
1935
+
1936
+ const document = await createDocumentFromMarkdownExport({
1937
+ filename: buildCopyColumnDocumentFilename(columnName),
1938
+ title: `${columnName || 'Column'} todos`,
1939
+ content: buildCopyColumnDocumentContent({
1940
+ state,
1941
+ columnName,
1942
+ text,
1943
+ valueCount,
1944
+ }),
1945
+ });
1946
+
1947
+ if (!document) {
1948
+ throw new Error('Document could not be created.');
1949
+ }
1950
+
1951
+ closeModal();
1952
+ showToast(
1953
+ `Document "${document.filename}" created · ${formatNumber(valueCount)} ${
1954
+ valueCount === 1 ? 'item' : 'items'
1955
+ }`,
1956
+ 'success',
1957
+ );
1958
+ router.navigate(`/documents/${encodeURIComponent(document.id)}`);
1959
+ return;
1960
+ }
1961
+
1962
+ if (isExportIntent) {
1963
+ const metadata = getCopyColumnExportMetadata(copyMode);
1964
+
1965
+ downloadTextFile({
1966
+ text,
1967
+ filename: buildCopyColumnExportFilename(columnName, copyMode),
1968
+ mimeType: metadata.mimeType,
1969
+ });
1970
+ closeModal();
1971
+ showToast(
1972
+ `Column "${columnName}" exported as ${metadata.label} · ${formatNumber(valueCount)} ${
1973
+ valueCount === 1 ? 'value' : 'values'
1974
+ }`,
1975
+ 'success',
1976
+ );
1977
+ return;
1978
+ }
1979
+
1980
+ if (!navigator.clipboard?.writeText) {
1981
+ throw new Error('Clipboard API is not available.');
1982
+ }
1983
+
1984
+ await navigator.clipboard.writeText(text);
1985
+ closeModal();
1986
+ showToast(
1987
+ `Column "${columnName}" copied · ${formatNumber(valueCount)} ${valueCount === 1 ? 'value' : 'values'}`,
1988
+ 'success',
1989
+ );
1990
+ } catch (error) {
1991
+ setCopyColumnModalError({
1992
+ code: isDocumentIntent
1993
+ ? 'COPY_COLUMN_DOCUMENT_EXPORT_FAILED'
1994
+ : isExportIntent
1995
+ ? 'COPY_COLUMN_EXPORT_FAILED'
1996
+ : 'CLIPBOARD_ACCESS_FAILED',
1997
+ message:
1998
+ error?.message ||
1999
+ (isDocumentIntent
2000
+ ? 'Document export failed.'
2001
+ : isExportIntent
2002
+ ? 'Column export failed.'
2003
+ : 'Clipboard access failed.'),
2004
+ });
2005
+ showToast(
2006
+ isDocumentIntent ? 'Document export failed.' : isExportIntent ? 'Column export failed.' : 'Clipboard access failed.',
2007
+ 'alert',
2008
+ );
2009
+ }
2010
+ }
2011
+
1294
2012
  async function handleAction(actionNode) {
1295
2013
  const { action } = actionNode.dataset;
1296
2014
 
@@ -1304,8 +2022,77 @@ async function handleAction(actionNode) {
1304
2022
  case 'open-row-editor-url':
1305
2023
  openRowEditorUrl(actionNode);
1306
2024
  return;
2025
+ case 'copy-row-editor-json':
2026
+ await copyRowEditorJson();
2027
+ return;
2028
+ case 'export-row-editor-json':
2029
+ exportRowEditorJson();
2030
+ return;
1307
2031
  case 'open-modal':
1308
- openModal(actionNode.dataset.modal);
2032
+ openModal(actionNode.dataset.modal, {
2033
+ columnId: actionNode.dataset.columnId,
2034
+ columnName: actionNode.dataset.columnName,
2035
+ });
2036
+ return;
2037
+ case 'open-copy-column-modal':
2038
+ closeCopyColumnMenus();
2039
+ openCopyColumnModal({
2040
+ scope: actionNode.dataset.resultScope,
2041
+ columnName: actionNode.dataset.columnName ?? '',
2042
+ mode: actionNode.dataset.copyMode,
2043
+ });
2044
+ return;
2045
+ case 'create-document': {
2046
+ const document = await createDocument();
2047
+
2048
+ if (document?.id) {
2049
+ router.navigate(`/documents/${encodeURIComponent(document.id)}`);
2050
+ }
2051
+ return;
2052
+ }
2053
+ case 'select-document': {
2054
+ const documentId = String(actionNode.dataset.documentId ?? '').trim();
2055
+
2056
+ if (documentId) {
2057
+ clearDocumentAutosaveTimer();
2058
+ router.navigate(`/documents/${encodeURIComponent(documentId)}`);
2059
+ }
2060
+ return;
2061
+ }
2062
+ case 'save-document':
2063
+ clearDocumentAutosaveTimer();
2064
+ await saveCurrentDocument();
2065
+ return;
2066
+ case 'export-document-markdown':
2067
+ exportCurrentDocumentMarkdown();
2068
+ return;
2069
+ case 'open-document-insert-table-modal':
2070
+ await openDocumentInsertTableModal(getCurrentDocumentEditorInsertionRange());
2071
+ return;
2072
+ case 'open-document-insert-note-modal':
2073
+ await openDocumentInsertNoteModal(getCurrentDocumentEditorInsertionRange());
2074
+ return;
2075
+ case 'import-document-markdown': {
2076
+ const fileInput = document.querySelector('[data-bind="document-import-file"]');
2077
+
2078
+ if (!(fileInput instanceof HTMLInputElement)) {
2079
+ return;
2080
+ }
2081
+
2082
+ fileInput.value = '';
2083
+ fileInput.click();
2084
+ return;
2085
+ }
2086
+ case 'delete-document':
2087
+ clearDocumentAutosaveTimer();
2088
+ openDeleteDocumentModal();
2089
+ return;
2090
+ case 'toggle-document-pane':
2091
+ toggleDocumentsPane(actionNode.dataset.pane);
2092
+ return;
2093
+ case 'toggle-document-todo':
2094
+ clearDocumentAutosaveTimer();
2095
+ await toggleCurrentDocumentTodo(actionNode.dataset.lineIndex);
1309
2096
  return;
1310
2097
  case 'open-create-query-chart-modal':
1311
2098
  openCreateQueryChartModal();
@@ -1329,6 +2116,7 @@ async function handleAction(actionNode) {
1329
2116
  dismissToast(actionNode.dataset.toastId);
1330
2117
  return;
1331
2118
  case 'select-connection': {
2119
+ closeSidebarDatabasePickers();
1332
2120
  resetStructureGraphForDatabaseChange();
1333
2121
  const next = await selectConnection(actionNode.dataset.connectionId);
1334
2122
  if (next) {
@@ -1384,6 +2172,9 @@ async function handleAction(actionNode) {
1384
2172
  await executeEditorQueryAndNavigate();
1385
2173
  return;
1386
2174
  }
2175
+ case 'format-current-query':
2176
+ formatCurrentQuery();
2177
+ return;
1387
2178
  case 'delete-data-row':
1388
2179
  openDeleteDataRowModal(actionNode.dataset.rowIndex);
1389
2180
  return;
@@ -1507,9 +2298,10 @@ async function handleAction(actionNode) {
1507
2298
  return;
1508
2299
  case 'export-query-format': {
1509
2300
  const format = actionNode.dataset.exportFormat;
2301
+ const filename = getExportFilenameFromAction(actionNode);
1510
2302
 
1511
2303
  if (format === 'table') {
1512
- const imported = await duplicateCurrentQueryAsTable();
2304
+ const imported = await duplicateCurrentQueryAsTable(filename);
1513
2305
 
1514
2306
  if (imported) {
1515
2307
  router.navigate('/table-designer/new');
@@ -1518,7 +2310,7 @@ async function handleAction(actionNode) {
1518
2310
  return;
1519
2311
  }
1520
2312
 
1521
- await exportCurrentQueryFormat(format);
2313
+ await exportCurrentQueryFormat(format, filename);
1522
2314
  return;
1523
2315
  }
1524
2316
  case 'open-data-export-modal':
@@ -1526,9 +2318,10 @@ async function handleAction(actionNode) {
1526
2318
  return;
1527
2319
  case 'export-data-format': {
1528
2320
  const format = actionNode.dataset.exportFormat;
2321
+ const filename = getExportFilenameFromAction(actionNode);
1529
2322
 
1530
2323
  if (format === 'table') {
1531
- const imported = await duplicateCurrentDataTableAsTable();
2324
+ const imported = await duplicateCurrentDataTableAsTable(filename);
1532
2325
 
1533
2326
  if (imported) {
1534
2327
  router.navigate('/table-designer/new');
@@ -1537,7 +2330,7 @@ async function handleAction(actionNode) {
1537
2330
  return;
1538
2331
  }
1539
2332
 
1540
- await exportCurrentDataTableFormat(format);
2333
+ await exportCurrentDataTableFormat(format, filename);
1541
2334
  return;
1542
2335
  }
1543
2336
  case 'toggle-data-tables':
@@ -1715,6 +2508,7 @@ async function handleAction(actionNode) {
1715
2508
  }
1716
2509
  return;
1717
2510
  case 'reload-data-route':
2511
+ preserveDataRowEditorSelectionForReload();
1718
2512
  await refreshCurrentRoute();
1719
2513
  return;
1720
2514
  default:
@@ -1735,7 +2529,37 @@ function canApplyMediaTaggingShortcut(state) {
1735
2529
  }
1736
2530
 
1737
2531
  document.addEventListener('click', event => {
1738
- const actionNode = event.target.closest('[data-action]');
2532
+ const target = event.target instanceof Element ? event.target : null;
2533
+
2534
+ if (!target) {
2535
+ return;
2536
+ }
2537
+
2538
+ const copyColumnMenu = target.closest('[data-copy-column-menu]');
2539
+
2540
+ if (!copyColumnMenu) {
2541
+ closeCopyColumnMenus();
2542
+ } else if (target.closest('.query-result-column-menu__toggle')) {
2543
+ window.requestAnimationFrame(() => {
2544
+ if (copyColumnMenu instanceof HTMLDetailsElement && copyColumnMenu.open) {
2545
+ closeCopyColumnMenus(copyColumnMenu);
2546
+ }
2547
+ });
2548
+ }
2549
+
2550
+ const sidebarDatabasePicker = target.closest('.sidebar-db-picker');
2551
+
2552
+ if (!sidebarDatabasePicker) {
2553
+ closeSidebarDatabasePickers();
2554
+ } else if (target.closest('.sidebar-footer-card')) {
2555
+ window.requestAnimationFrame(() => {
2556
+ if (sidebarDatabasePicker instanceof HTMLDetailsElement && sidebarDatabasePicker.open) {
2557
+ closeSidebarDatabasePickers(sidebarDatabasePicker);
2558
+ }
2559
+ });
2560
+ }
2561
+
2562
+ const actionNode = target.closest('[data-action]');
1739
2563
 
1740
2564
  if (!actionNode) {
1741
2565
  return;
@@ -1744,10 +2568,36 @@ document.addEventListener('click', event => {
1744
2568
  handleAction(actionNode);
1745
2569
  });
1746
2570
 
2571
+ document.addEventListener('contextmenu', event => {
2572
+ const target = event.target instanceof Element ? event.target : null;
2573
+ const headerNode = target?.closest('[data-result-column-header]');
2574
+
2575
+ if (!headerNode) {
2576
+ return;
2577
+ }
2578
+
2579
+ event.preventDefault();
2580
+ openCopyColumnMenu(headerNode);
2581
+ });
2582
+
1747
2583
  document.addEventListener('keydown', event => {
1748
2584
  const target = event.target;
1749
2585
  const state = getState();
1750
2586
 
2587
+ if (
2588
+ (event.key === 's' || event.key === 'S') &&
2589
+ (event.ctrlKey || event.metaKey) &&
2590
+ !event.altKey &&
2591
+ !event.shiftKey &&
2592
+ !event.defaultPrevented &&
2593
+ state.route.name === 'documents'
2594
+ ) {
2595
+ event.preventDefault();
2596
+ clearDocumentAutosaveTimer();
2597
+ void saveCurrentDocument();
2598
+ return;
2599
+ }
2600
+
1751
2601
  // Handle Enter key in tag form fields to trigger create tag
1752
2602
  if (
1753
2603
  event.key === 'Enter' &&
@@ -1810,6 +2660,18 @@ document.addEventListener('keydown', event => {
1810
2660
  return;
1811
2661
  }
1812
2662
 
2663
+ if (document.querySelector('[data-copy-column-menu][open]')) {
2664
+ event.preventDefault();
2665
+ closeCopyColumnMenus();
2666
+ return;
2667
+ }
2668
+
2669
+ if (document.querySelector('.sidebar-db-picker[open]')) {
2670
+ event.preventDefault();
2671
+ closeSidebarDatabasePickers();
2672
+ return;
2673
+ }
2674
+
1813
2675
  if (
1814
2676
  state.route.name === 'data' &&
1815
2677
  (typeof state.dataBrowser.selectedRowIndex === 'number' || Boolean(state.dataBrowser.selectedRow))
@@ -1826,12 +2688,33 @@ document.addEventListener('keydown', event => {
1826
2688
  });
1827
2689
 
1828
2690
  document.addEventListener('input', event => {
2691
+ const target = event.target instanceof Element ? event.target : null;
2692
+ const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
2693
+ const textCellInput = target?.closest('[data-row-editor-text-source]');
2694
+
2695
+ if (timestampInput) {
2696
+ syncRowEditorTimestampPreview(timestampInput);
2697
+ syncRowEditorFilePathPreview(timestampInput);
2698
+ }
2699
+
2700
+ if (textCellInput) {
2701
+ syncRowEditorCharacterCount(textCellInput);
2702
+ }
2703
+
1829
2704
  const bindNode = event.target.closest('[data-bind]');
1830
2705
 
1831
2706
  if (!bindNode) {
1832
2707
  return;
1833
2708
  }
1834
2709
 
2710
+ if (bindNode.dataset.bind === 'copy-column-format-field') {
2711
+ updateCopyColumnModalFormatField(
2712
+ bindNode.dataset.field,
2713
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : bindNode.value,
2714
+ );
2715
+ return;
2716
+ }
2717
+
1835
2718
  if (bindNode.dataset.bind === 'current-query') {
1836
2719
  invalidateMainRenderCache();
1837
2720
  syncQueryEditorHighlight(bindNode);
@@ -1840,6 +2723,12 @@ document.addEventListener('input', event => {
1840
2723
  return;
1841
2724
  }
1842
2725
 
2726
+ if (bindNode.dataset.bind === 'document-field') {
2727
+ updateCurrentDocumentDraftField(bindNode.dataset.field, bindNode.value);
2728
+ scheduleDocumentAutosave(getState().documents.selectedId);
2729
+ return;
2730
+ }
2731
+
1843
2732
  if (bindNode.dataset.bind === 'data-search-query') {
1844
2733
  void setDataSearchQuery(bindNode.value);
1845
2734
  return;
@@ -1873,7 +2762,12 @@ document.addEventListener('input', event => {
1873
2762
  return;
1874
2763
  }
1875
2764
 
1876
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
2765
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
2766
+
2767
+ if (!syncTableDesignerDraftUi(bindNode)) {
2768
+ renderApp(getState());
2769
+ }
2770
+
1877
2771
  return;
1878
2772
  }
1879
2773
 
@@ -1882,7 +2776,14 @@ document.addEventListener('input', event => {
1882
2776
  return;
1883
2777
  }
1884
2778
 
1885
- updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
2779
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
2780
+ notify: false,
2781
+ });
2782
+
2783
+ if (!syncTableDesignerDraftUi(bindNode)) {
2784
+ renderApp(getState());
2785
+ }
2786
+
1886
2787
  return;
1887
2788
  }
1888
2789
 
@@ -1943,6 +2844,19 @@ document.addEventListener(
1943
2844
  );
1944
2845
 
1945
2846
  document.addEventListener('change', event => {
2847
+ const target = event.target instanceof Element ? event.target : null;
2848
+ const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
2849
+ const textCellInput = target?.closest('[data-row-editor-text-source]');
2850
+
2851
+ if (timestampInput) {
2852
+ syncRowEditorTimestampPreview(timestampInput);
2853
+ syncRowEditorFilePathPreview(timestampInput);
2854
+ }
2855
+
2856
+ if (textCellInput) {
2857
+ syncRowEditorCharacterCount(textCellInput);
2858
+ }
2859
+
1946
2860
  const bindNode = event.target.closest('[data-bind]');
1947
2861
 
1948
2862
  if (!bindNode) {
@@ -1985,6 +2899,16 @@ document.addEventListener('change', event => {
1985
2899
  return;
1986
2900
  }
1987
2901
 
2902
+ if (bindNode.dataset.bind === 'document-import-file') {
2903
+ void handleDocumentMarkdownImport(bindNode);
2904
+ return;
2905
+ }
2906
+
2907
+ if (bindNode.dataset.bind === 'document-insert-query-select') {
2908
+ updateDocumentInsertQuerySelection(bindNode.value);
2909
+ return;
2910
+ }
2911
+
1988
2912
  if (bindNode.dataset.bind === 'table-designer-field') {
1989
2913
  if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
1990
2914
  updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
@@ -2001,7 +2925,12 @@ document.addEventListener('change', event => {
2001
2925
  return;
2002
2926
  }
2003
2927
 
2004
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
2928
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
2929
+
2930
+ if (!syncTableDesignerDraftUi(bindNode)) {
2931
+ renderApp(getState());
2932
+ }
2933
+
2005
2934
  return;
2006
2935
  }
2007
2936
 
@@ -2018,7 +2947,14 @@ document.addEventListener('change', event => {
2018
2947
  return;
2019
2948
  }
2020
2949
 
2021
- updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
2950
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
2951
+ notify: false,
2952
+ });
2953
+
2954
+ if (!syncTableDesignerDraftUi(bindNode)) {
2955
+ renderApp(getState());
2956
+ }
2957
+
2022
2958
  return;
2023
2959
  }
2024
2960
 
@@ -2027,6 +2963,16 @@ document.addEventListener('change', event => {
2027
2963
  return;
2028
2964
  }
2029
2965
 
2966
+ if (bindNode.dataset.bind === 'table-designer-constraint-field') {
2967
+ updateCurrentTableDesignerConstraintField(
2968
+ bindNode.dataset.constraintKind,
2969
+ bindNode.dataset.constraintId,
2970
+ bindNode.dataset.field,
2971
+ bindNode.value,
2972
+ );
2973
+ return;
2974
+ }
2975
+
2030
2976
  if (bindNode.dataset.bind === 'query-chart-draft:chartType') {
2031
2977
  updateCurrentQueryChartDraftField('chartType', bindNode.value);
2032
2978
  return;
@@ -2057,7 +3003,8 @@ document.addEventListener('submit', async event => {
2057
3003
  }
2058
3004
 
2059
3005
  event.preventDefault();
2060
- const formData = new FormData(form);
3006
+ const submitter = event.submitter instanceof HTMLElement ? event.submitter : null;
3007
+ const formData = submitter ? new FormData(form, submitter) : new FormData(form);
2061
3008
 
2062
3009
  switch (form.dataset.form) {
2063
3010
  case 'open-connection': {
@@ -2140,6 +3087,32 @@ document.addEventListener('submit', async event => {
2140
3087
  case 'delete-query-history-confirm':
2141
3088
  await submitDeleteQueryHistoryConfirmation();
2142
3089
  return;
3090
+ case 'delete-document-confirm': {
3091
+ const result = await submitDeleteDocumentConfirmation();
3092
+
3093
+ if (result.deleted) {
3094
+ router.navigate(
3095
+ result.nextDocumentId ? `/documents/${encodeURIComponent(result.nextDocumentId)}` : '/documents',
3096
+ );
3097
+ }
3098
+ return;
3099
+ }
3100
+ case 'document-insert-table': {
3101
+ const inserted = await submitDocumentInsertTable();
3102
+
3103
+ if (inserted) {
3104
+ scheduleDocumentAutosave(getState().documents.selectedId);
3105
+ }
3106
+ return;
3107
+ }
3108
+ case 'document-insert-note': {
3109
+ const inserted = submitDocumentInsertNote();
3110
+
3111
+ if (inserted) {
3112
+ scheduleDocumentAutosave(getState().documents.selectedId);
3113
+ }
3114
+ return;
3115
+ }
2143
3116
  case 'apply-row-update-preview':
2144
3117
  await submitRowUpdatePreviewConfirmation();
2145
3118
  return;
@@ -2149,6 +3122,9 @@ document.addEventListener('submit', async event => {
2149
3122
  case 'create-media-tagging-mapping-table':
2150
3123
  await submitCreateMediaTaggingMappingTable();
2151
3124
  return;
3125
+ case 'copy-column':
3126
+ await submitCopyColumnModal(formData);
3127
+ return;
2152
3128
  case 'save-data-row': {
2153
3129
  const values = {};
2154
3130