sqlite-hub 0.12.0 → 0.16.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 (46) hide show
  1. package/README.md +12 -4
  2. package/frontend/js/api.js +4 -2
  3. package/frontend/js/app.js +632 -12
  4. package/frontend/js/components/modal.js +432 -3
  5. package/frontend/js/components/queryEditor.js +9 -1
  6. package/frontend/js/components/queryResults.js +79 -17
  7. package/frontend/js/components/rowEditorPanel.js +204 -10
  8. package/frontend/js/components/sidebar.js +101 -18
  9. package/frontend/js/components/tableDesignerEditor.js +69 -11
  10. package/frontend/js/store.js +227 -9
  11. package/frontend/js/utils/copyColumnExport.js +117 -0
  12. package/frontend/js/utils/exportFilenames.js +32 -0
  13. package/frontend/js/utils/filePathPreview.js +315 -0
  14. package/frontend/js/utils/format.js +1 -1
  15. package/frontend/js/utils/rowEditorJson.js +65 -0
  16. package/frontend/js/utils/sqlFormatter.js +691 -0
  17. package/frontend/js/utils/tableDesigner.js +178 -6
  18. package/frontend/js/utils/textCellStats.js +20 -0
  19. package/frontend/js/utils/timestampPreview.js +264 -0
  20. package/frontend/js/views/charts.js +3 -1
  21. package/frontend/js/views/data.js +34 -2
  22. package/frontend/js/views/editor.js +48 -1
  23. package/frontend/js/views/settings.js +0 -3
  24. package/frontend/js/views/structure.js +154 -212
  25. package/frontend/styles/base.css +6 -0
  26. package/frontend/styles/components.css +463 -2
  27. package/frontend/styles/structure-graph.css +0 -3
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +1 -1
  30. package/package.json +2 -3
  31. package/server/services/sqlite/introspection.js +10 -0
  32. package/server/services/sqlite/sqlExecutor.js +29 -0
  33. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  34. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  35. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  36. package/server/services/sqlite/tableDesignerService.js +1 -1
  37. package/tests/check-constraint-options.test.js +14 -0
  38. package/tests/export-filenames.test.js +34 -0
  39. package/tests/file-path-preview.test.js +165 -0
  40. package/tests/row-editor-json.test.js +82 -0
  41. package/tests/row-editor-timestamp-preview.test.js +192 -0
  42. package/tests/sql-formatter.test.js +173 -0
  43. package/tests/sql-highlight.test.js +38 -0
  44. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  45. package/tests/text-cell-stats.test.js +38 -0
  46. package/fill.js +0 -526
@@ -52,7 +52,9 @@ import {
52
52
  openDataRowByIdentity,
53
53
  openEditConnectionModal,
54
54
  openCreateQueryChartModal,
55
+ openCopyColumnModal,
55
56
  openEditQueryChartModal,
57
+ preserveCurrentDataRowSelectionForReload,
56
58
  openDataRowUpdatePreview,
57
59
  openEditorRowUpdatePreview,
58
60
  refreshCurrentRoute,
@@ -97,6 +99,8 @@ import {
97
99
  sortEditorResultsByColumn,
98
100
  setQueryHistorySearchInput,
99
101
  setQueryHistoryTab,
102
+ setCopyColumnModalError,
103
+ setCopyColumnModalSubmitting,
100
104
  setRoute,
101
105
  saveQueryHistoryNotes,
102
106
  saveQueryHistoryTitle,
@@ -107,6 +111,7 @@ import {
107
111
  setMediaTaggingWorkflowMediaRotationDegrees,
108
112
  queueTableDesignerCsvImport,
109
113
  showToast,
114
+ storeCopyColumnPreferences,
110
115
  submitCreateConnection,
111
116
  createCurrentMediaTag,
112
117
  submitDeleteRowConfirmation,
@@ -118,9 +123,11 @@ import {
118
123
  toggleQueryHistorySavedState,
119
124
  updateCurrentMediaTaggingField,
120
125
  updateCurrentMediaTaggingTagFormField,
126
+ updateCopyColumnModalFormatField,
121
127
  updateCurrentQueryChartDraftConfigField,
122
128
  updateCurrentQueryChartDraftField,
123
129
  updateCurrentTableDesignerColumnField,
130
+ updateCurrentTableDesignerConstraintField,
124
131
  updateCurrentTableDesignerField,
125
132
  addCurrentTableDesignerColumn,
126
133
  applyCurrentMediaTaggingSelection,
@@ -137,7 +144,29 @@ import { renderSettingsView } from './views/settings.js';
137
144
  import { renderStructureView } from './views/structure.js';
138
145
  import { renderTableDesignerView } from './views/tableDesigner.js';
139
146
  import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
140
- import { highlightSql } from './utils/format.js';
147
+ import {
148
+ buildCopyColumnText,
149
+ getCopyColumnExportMetadata,
150
+ isMarkdownTodoCopyColumnMode,
151
+ normalizeCopyColumnMode,
152
+ } from './utils/copyColumnExport.js';
153
+ import { formatNumber, highlightSql } from './utils/format.js';
154
+ import {
155
+ compactPathForDisplay,
156
+ detectFilePathValue,
157
+ getPathTypeLabel,
158
+ } from './utils/filePathPreview.js';
159
+ import { formatSqlQuery } from './utils/sqlFormatter.js';
160
+ import {
161
+ buildDataRowEditorJsonObject,
162
+ buildEditorRowEditorJsonObject,
163
+ stringifyRowEditorJson,
164
+ } from './utils/rowEditorJson.js';
165
+ import { getTimestampPreviewForField } from './utils/timestampPreview.js';
166
+ import {
167
+ formatTextCellCharacterCount,
168
+ getTextCellCharacterCount,
169
+ } from './utils/textCellStats.js';
141
170
 
142
171
  const appRoot = document.querySelector('#app');
143
172
 
@@ -1254,6 +1283,39 @@ async function executeEditorQueryAndNavigate() {
1254
1283
  router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
1255
1284
  }
1256
1285
 
1286
+ function formatCurrentQuery() {
1287
+ const currentQuery = getState().editor.sqlText ?? '';
1288
+ const formattedQuery = formatSqlQuery(currentQuery);
1289
+
1290
+ if (!formattedQuery) {
1291
+ showToast('No SQL query to format.', 'alert');
1292
+ return;
1293
+ }
1294
+
1295
+ if (formattedQuery === currentQuery) {
1296
+ focusQueryEditorInput();
1297
+ showToast('SQL query is already formatted.', 'muted');
1298
+ return;
1299
+ }
1300
+
1301
+ invalidateMainRenderCache();
1302
+ setCurrentQuery(formattedQuery);
1303
+
1304
+ const input = document.querySelector('[data-bind="current-query"]');
1305
+
1306
+ if (input instanceof HTMLTextAreaElement) {
1307
+ input.value = formattedQuery;
1308
+ syncQueryEditorHighlight(input);
1309
+ syncQueryEditorScroll(input);
1310
+ input.focus({ preventScroll: true });
1311
+ input.setSelectionRange(input.value.length, input.value.length);
1312
+ } else {
1313
+ pendingQueryEditorFocus = true;
1314
+ }
1315
+
1316
+ showToast('SQL query formatted.', 'success');
1317
+ }
1318
+
1257
1319
  const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
1258
1320
 
1259
1321
  function getOpenableUrl(value) {
@@ -1291,6 +1353,414 @@ function openRowEditorUrl(actionNode) {
1291
1353
  link.remove();
1292
1354
  }
1293
1355
 
1356
+ function getExportFilenameFromAction(actionNode) {
1357
+ const input = actionNode.closest('[data-export-modal]')?.querySelector('input[name="filename"]');
1358
+ return input instanceof HTMLInputElement ? input.value : '';
1359
+ }
1360
+
1361
+ function syncRowEditorTimestampPreview(inputNode) {
1362
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1363
+ const previewNode = fieldNode?.querySelector('[data-row-editor-timestamp-preview]');
1364
+
1365
+ if (!fieldNode || !previewNode) {
1366
+ return;
1367
+ }
1368
+
1369
+ const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
1370
+ const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
1371
+ const tableMeta = {
1372
+ columns: [
1373
+ {
1374
+ name: columnName,
1375
+ primaryKeyPosition: protectedKeyColumn ? 1 : 0,
1376
+ foreignKey: protectedKeyColumn,
1377
+ },
1378
+ ],
1379
+ foreignKeys: [],
1380
+ };
1381
+ const preview = getTimestampPreviewForField({
1382
+ columnName,
1383
+ value: inputNode.value,
1384
+ tableMeta,
1385
+ });
1386
+
1387
+ if (preview.kind !== 'timestamp') {
1388
+ previewNode.hidden = true;
1389
+ previewNode.textContent = '';
1390
+ return;
1391
+ }
1392
+
1393
+ previewNode.hidden = false;
1394
+ previewNode.textContent = `Interpretiert als Datum: ${preview.formatted}`;
1395
+ }
1396
+
1397
+ function syncRowEditorCharacterCount(inputNode) {
1398
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1399
+ const countNode = fieldNode?.querySelector('[data-row-editor-char-count]');
1400
+
1401
+ if (!fieldNode || !countNode) {
1402
+ return;
1403
+ }
1404
+
1405
+ const count = getTextCellCharacterCount(inputNode.value);
1406
+
1407
+ if (count === null) {
1408
+ countNode.hidden = true;
1409
+ countNode.textContent = '';
1410
+ return;
1411
+ }
1412
+
1413
+ countNode.hidden = false;
1414
+ countNode.textContent = formatTextCellCharacterCount(count);
1415
+ }
1416
+
1417
+ function syncRowEditorFilePathPreview(inputNode) {
1418
+ const fieldNode = inputNode.closest('[data-row-editor-field]');
1419
+ const previewNode = fieldNode?.querySelector('[data-row-editor-filepath-preview]');
1420
+
1421
+ if (!fieldNode || !previewNode) {
1422
+ return;
1423
+ }
1424
+
1425
+ const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
1426
+ const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
1427
+ const tableMeta = {
1428
+ columns: [
1429
+ {
1430
+ name: columnName,
1431
+ primaryKeyPosition: protectedKeyColumn ? 1 : 0,
1432
+ foreignKey: protectedKeyColumn,
1433
+ },
1434
+ ],
1435
+ foreignKeys: [],
1436
+ };
1437
+ const preview = detectFilePathValue(inputNode.value, columnName, tableMeta);
1438
+
1439
+ if (!preview) {
1440
+ previewNode.hidden = true;
1441
+ return;
1442
+ }
1443
+
1444
+ const filenameNode = previewNode.querySelector('[data-row-editor-filepath-filename]');
1445
+ const directoryRowNode = previewNode.querySelector('[data-row-editor-filepath-directory-row]');
1446
+ const directoryNode = previewNode.querySelector('[data-row-editor-filepath-directory]');
1447
+ const extensionNode = previewNode.querySelector('[data-row-editor-filepath-extension]');
1448
+ const typeNode = previewNode.querySelector('[data-row-editor-filepath-type]');
1449
+
1450
+ if (filenameNode) {
1451
+ filenameNode.textContent = preview.fileName ?? 'N/A';
1452
+ }
1453
+
1454
+ if (directoryRowNode) {
1455
+ directoryRowNode.hidden = !preview.directory;
1456
+ }
1457
+
1458
+ if (directoryNode) {
1459
+ directoryNode.textContent = preview.directory ? compactPathForDisplay(preview.directory, 72) : '';
1460
+ directoryNode.setAttribute('title', preview.directory ?? '');
1461
+ }
1462
+
1463
+ if (extensionNode) {
1464
+ extensionNode.textContent = preview.extension ?? 'N/A';
1465
+ }
1466
+
1467
+ if (typeNode) {
1468
+ typeNode.textContent = getPathTypeLabel(preview.pathType);
1469
+ }
1470
+
1471
+ previewNode.hidden = false;
1472
+ }
1473
+
1474
+ function closeCopyColumnMenus(exceptMenu = null) {
1475
+ document.querySelectorAll('[data-copy-column-menu][open]').forEach(menu => {
1476
+ if (menu !== exceptMenu && menu instanceof HTMLDetailsElement) {
1477
+ menu.open = false;
1478
+ }
1479
+ });
1480
+ }
1481
+
1482
+ function closeSidebarDatabasePickers(exceptPicker = null) {
1483
+ document.querySelectorAll('.sidebar-db-picker[open]').forEach(picker => {
1484
+ if (picker !== exceptPicker && picker instanceof HTMLDetailsElement) {
1485
+ picker.open = false;
1486
+ }
1487
+ });
1488
+ }
1489
+
1490
+ function openCopyColumnMenu(headerNode) {
1491
+ const menu = headerNode?.querySelector('[data-copy-column-menu]');
1492
+
1493
+ if (!(menu instanceof HTMLDetailsElement)) {
1494
+ return;
1495
+ }
1496
+
1497
+ closeCopyColumnMenus(menu);
1498
+ menu.open = true;
1499
+ menu.querySelector('summary')?.focus({ preventScroll: true });
1500
+ }
1501
+
1502
+ function getCopyColumnResult(state, scope) {
1503
+ return scope === 'charts' ? state.charts.result : state.editor.result;
1504
+ }
1505
+
1506
+ function slugifyExportFilenamePart(value, fallback = 'column') {
1507
+ const slug = String(value ?? '')
1508
+ .trim()
1509
+ .toLowerCase()
1510
+ .replace(/[^a-z0-9]+/g, '-')
1511
+ .replace(/^-+|-+$/g, '');
1512
+
1513
+ return slug || fallback;
1514
+ }
1515
+
1516
+ function buildCopyColumnExportFilename(columnName, copyMode) {
1517
+ const metadata = getCopyColumnExportMetadata(copyMode);
1518
+ const columnSlug = slugifyExportFilenamePart(columnName);
1519
+
1520
+ return `${columnSlug}-${metadata.suffix}.${metadata.extension}`;
1521
+ }
1522
+
1523
+ function downloadTextFile({ text, filename, mimeType }) {
1524
+ const blob = new Blob([text], { type: mimeType });
1525
+ const url = URL.createObjectURL(blob);
1526
+ const link = document.createElement('a');
1527
+
1528
+ link.href = url;
1529
+ link.download = filename;
1530
+ document.body.appendChild(link);
1531
+ link.click();
1532
+ link.remove();
1533
+ URL.revokeObjectURL(url);
1534
+ }
1535
+
1536
+ function getSelectedDataBrowserRowForJson(state) {
1537
+ if (state.dataBrowser.selectedRow) {
1538
+ return state.dataBrowser.selectedRow;
1539
+ }
1540
+
1541
+ const rowIndex = state.dataBrowser.selectedRowIndex;
1542
+
1543
+ if (typeof rowIndex !== 'number') {
1544
+ return null;
1545
+ }
1546
+
1547
+ return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
1548
+ }
1549
+
1550
+ function getSelectedEditorResultRowForJson(state) {
1551
+ const rowIndex = state.editor.selectedRowIndex;
1552
+
1553
+ if (typeof rowIndex !== 'number') {
1554
+ return null;
1555
+ }
1556
+
1557
+ return state.editor.result?.rows?.[rowIndex] ?? null;
1558
+ }
1559
+
1560
+ function applyChangedRowEditorFormValues(rowObject) {
1561
+ const form = shellRefs.panel.querySelector(
1562
+ 'form[data-form="save-data-row"], form[data-form="save-editor-row"]',
1563
+ );
1564
+
1565
+ if (!form) {
1566
+ return rowObject;
1567
+ }
1568
+
1569
+ const nextRowObject = { ...rowObject };
1570
+ const formData = new FormData(form);
1571
+
1572
+ for (const [key, value] of formData.entries()) {
1573
+ if (!key.startsWith('field:')) {
1574
+ continue;
1575
+ }
1576
+
1577
+ const control = Array.from(form.elements).find(element => element.name === key);
1578
+ const initialValue = control?.dataset?.rowEditorInitialValue;
1579
+ const currentValue = String(value ?? '');
1580
+
1581
+ if (initialValue !== undefined && currentValue === initialValue) {
1582
+ continue;
1583
+ }
1584
+
1585
+ nextRowObject[key.slice('field:'.length)] = currentValue;
1586
+ }
1587
+
1588
+ return nextRowObject;
1589
+ }
1590
+
1591
+ function buildRowEditorJsonPayload(state) {
1592
+ if (state.route.name === 'data') {
1593
+ const row = getSelectedDataBrowserRowForJson(state);
1594
+ const table = state.dataBrowser.table;
1595
+ const tableName = table?.name ?? state.dataBrowser.selectedTable ?? 'row';
1596
+
1597
+ if (!row || !table) {
1598
+ return null;
1599
+ }
1600
+
1601
+ const rowObject = applyChangedRowEditorFormValues(
1602
+ buildDataRowEditorJsonObject({
1603
+ row,
1604
+ columns: table.columns ?? table.columnMeta ?? [],
1605
+ }),
1606
+ );
1607
+
1608
+ return {
1609
+ filename: `${slugifyExportFilenamePart(tableName, 'row')}-row.json`,
1610
+ label: tableName,
1611
+ text: stringifyRowEditorJson(rowObject),
1612
+ };
1613
+ }
1614
+
1615
+ if (state.route.name === 'editorResults') {
1616
+ const row = getSelectedEditorResultRowForJson(state);
1617
+ const result = state.editor.result;
1618
+ const rowIndex = state.editor.selectedRowIndex;
1619
+ const tableName = result?.editing?.tableName ?? 'query-result';
1620
+
1621
+ if (!row || !result) {
1622
+ return null;
1623
+ }
1624
+
1625
+ const rowObject = applyChangedRowEditorFormValues(
1626
+ buildEditorRowEditorJsonObject({
1627
+ row,
1628
+ editingColumns: result.editing?.columns ?? [],
1629
+ resultColumns: result.columns ?? [],
1630
+ }),
1631
+ );
1632
+
1633
+ return {
1634
+ filename: `${slugifyExportFilenamePart(tableName, 'query-result')}-row-${Number(rowIndex) + 1}.json`,
1635
+ label: tableName,
1636
+ text: stringifyRowEditorJson(rowObject),
1637
+ };
1638
+ }
1639
+
1640
+ return null;
1641
+ }
1642
+
1643
+ async function copyRowEditorJson() {
1644
+ const payload = buildRowEditorJsonPayload(getState());
1645
+
1646
+ if (!payload) {
1647
+ showToast('No row is selected.', 'alert');
1648
+ return;
1649
+ }
1650
+
1651
+ if (!navigator.clipboard?.writeText) {
1652
+ showToast('Clipboard API is not available.', 'alert');
1653
+ return;
1654
+ }
1655
+
1656
+ try {
1657
+ await navigator.clipboard.writeText(payload.text);
1658
+ showToast(`Row from "${payload.label}" copied as JSON.`, 'success');
1659
+ } catch (error) {
1660
+ showToast('Row JSON could not be copied.', 'alert');
1661
+ }
1662
+ }
1663
+
1664
+ function exportRowEditorJson() {
1665
+ const payload = buildRowEditorJsonPayload(getState());
1666
+
1667
+ if (!payload) {
1668
+ showToast('No row is selected.', 'alert');
1669
+ return;
1670
+ }
1671
+
1672
+ downloadTextFile({
1673
+ text: payload.text,
1674
+ filename: payload.filename,
1675
+ mimeType: 'application/json;charset=utf-8',
1676
+ });
1677
+ showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
1678
+ }
1679
+
1680
+ function preserveDataRowEditorSelectionForReload() {
1681
+ preserveCurrentDataRowSelectionForReload();
1682
+ }
1683
+
1684
+ async function submitCopyColumnModal(formData) {
1685
+ const state = getState();
1686
+ const modal = state.modal;
1687
+
1688
+ if (modal?.kind !== 'copy-column') {
1689
+ return;
1690
+ }
1691
+
1692
+ const scope = String(formData.get('scope') ?? modal.scope ?? 'editor');
1693
+ const columnName = String(formData.get('columnName') ?? modal.columnName ?? '');
1694
+ const copyMode = normalizeCopyColumnMode(String(formData.get('copyMode') ?? modal.copyMode ?? 'column'));
1695
+ const separator = String(formData.get('separator') ?? ',');
1696
+ const wrapper = String(formData.get('wrapper') ?? '');
1697
+ const lineBreaks = formData.get('lineBreaks') === 'on';
1698
+ const outputSeparator = lineBreaks ? '\n' : separator;
1699
+ const intent = String(formData.get('intent') ?? 'copy');
1700
+ const isExportIntent = intent === 'export';
1701
+ const result = getCopyColumnResult(state, scope);
1702
+ const hasColumn = (result?.columns ?? []).some(column => String(column) === columnName);
1703
+
1704
+ if (!hasColumn) {
1705
+ setCopyColumnModalError({
1706
+ code: 'COPY_COLUMN_UNAVAILABLE',
1707
+ message: 'Column is no longer available in the current result set.',
1708
+ });
1709
+ showToast('Column could not be copied.', 'alert');
1710
+ return;
1711
+ }
1712
+
1713
+ if (!isMarkdownTodoCopyColumnMode(copyMode)) {
1714
+ storeCopyColumnPreferences({ separator, wrapper, lineBreaks });
1715
+ }
1716
+ setCopyColumnModalSubmitting(true);
1717
+
1718
+ const { text, valueCount } = buildCopyColumnText({
1719
+ result,
1720
+ columnName,
1721
+ copyMode,
1722
+ separator: outputSeparator,
1723
+ wrapper,
1724
+ });
1725
+
1726
+ try {
1727
+ if (isExportIntent) {
1728
+ const metadata = getCopyColumnExportMetadata(copyMode);
1729
+
1730
+ downloadTextFile({
1731
+ text,
1732
+ filename: buildCopyColumnExportFilename(columnName, copyMode),
1733
+ mimeType: metadata.mimeType,
1734
+ });
1735
+ closeModal();
1736
+ showToast(
1737
+ `Column "${columnName}" exported as ${metadata.label} · ${formatNumber(valueCount)} ${
1738
+ valueCount === 1 ? 'value' : 'values'
1739
+ }`,
1740
+ 'success',
1741
+ );
1742
+ return;
1743
+ }
1744
+
1745
+ if (!navigator.clipboard?.writeText) {
1746
+ throw new Error('Clipboard API is not available.');
1747
+ }
1748
+
1749
+ await navigator.clipboard.writeText(text);
1750
+ closeModal();
1751
+ showToast(
1752
+ `Column "${columnName}" copied · ${formatNumber(valueCount)} ${valueCount === 1 ? 'value' : 'values'}`,
1753
+ 'success',
1754
+ );
1755
+ } catch (error) {
1756
+ setCopyColumnModalError({
1757
+ code: isExportIntent ? 'COPY_COLUMN_EXPORT_FAILED' : 'CLIPBOARD_ACCESS_FAILED',
1758
+ message: error?.message || (isExportIntent ? 'Column export failed.' : 'Clipboard access failed.'),
1759
+ });
1760
+ showToast(isExportIntent ? 'Column export failed.' : 'Clipboard access failed.', 'alert');
1761
+ }
1762
+ }
1763
+
1294
1764
  async function handleAction(actionNode) {
1295
1765
  const { action } = actionNode.dataset;
1296
1766
 
@@ -1304,8 +1774,25 @@ async function handleAction(actionNode) {
1304
1774
  case 'open-row-editor-url':
1305
1775
  openRowEditorUrl(actionNode);
1306
1776
  return;
1777
+ case 'copy-row-editor-json':
1778
+ await copyRowEditorJson();
1779
+ return;
1780
+ case 'export-row-editor-json':
1781
+ exportRowEditorJson();
1782
+ return;
1307
1783
  case 'open-modal':
1308
- openModal(actionNode.dataset.modal);
1784
+ openModal(actionNode.dataset.modal, {
1785
+ columnId: actionNode.dataset.columnId,
1786
+ columnName: actionNode.dataset.columnName,
1787
+ });
1788
+ return;
1789
+ case 'open-copy-column-modal':
1790
+ closeCopyColumnMenus();
1791
+ openCopyColumnModal({
1792
+ scope: actionNode.dataset.resultScope,
1793
+ columnName: actionNode.dataset.columnName ?? '',
1794
+ mode: actionNode.dataset.copyMode,
1795
+ });
1309
1796
  return;
1310
1797
  case 'open-create-query-chart-modal':
1311
1798
  openCreateQueryChartModal();
@@ -1329,6 +1816,7 @@ async function handleAction(actionNode) {
1329
1816
  dismissToast(actionNode.dataset.toastId);
1330
1817
  return;
1331
1818
  case 'select-connection': {
1819
+ closeSidebarDatabasePickers();
1332
1820
  resetStructureGraphForDatabaseChange();
1333
1821
  const next = await selectConnection(actionNode.dataset.connectionId);
1334
1822
  if (next) {
@@ -1384,6 +1872,9 @@ async function handleAction(actionNode) {
1384
1872
  await executeEditorQueryAndNavigate();
1385
1873
  return;
1386
1874
  }
1875
+ case 'format-current-query':
1876
+ formatCurrentQuery();
1877
+ return;
1387
1878
  case 'delete-data-row':
1388
1879
  openDeleteDataRowModal(actionNode.dataset.rowIndex);
1389
1880
  return;
@@ -1507,9 +1998,10 @@ async function handleAction(actionNode) {
1507
1998
  return;
1508
1999
  case 'export-query-format': {
1509
2000
  const format = actionNode.dataset.exportFormat;
2001
+ const filename = getExportFilenameFromAction(actionNode);
1510
2002
 
1511
2003
  if (format === 'table') {
1512
- const imported = await duplicateCurrentQueryAsTable();
2004
+ const imported = await duplicateCurrentQueryAsTable(filename);
1513
2005
 
1514
2006
  if (imported) {
1515
2007
  router.navigate('/table-designer/new');
@@ -1518,7 +2010,7 @@ async function handleAction(actionNode) {
1518
2010
  return;
1519
2011
  }
1520
2012
 
1521
- await exportCurrentQueryFormat(format);
2013
+ await exportCurrentQueryFormat(format, filename);
1522
2014
  return;
1523
2015
  }
1524
2016
  case 'open-data-export-modal':
@@ -1526,9 +2018,10 @@ async function handleAction(actionNode) {
1526
2018
  return;
1527
2019
  case 'export-data-format': {
1528
2020
  const format = actionNode.dataset.exportFormat;
2021
+ const filename = getExportFilenameFromAction(actionNode);
1529
2022
 
1530
2023
  if (format === 'table') {
1531
- const imported = await duplicateCurrentDataTableAsTable();
2024
+ const imported = await duplicateCurrentDataTableAsTable(filename);
1532
2025
 
1533
2026
  if (imported) {
1534
2027
  router.navigate('/table-designer/new');
@@ -1537,7 +2030,7 @@ async function handleAction(actionNode) {
1537
2030
  return;
1538
2031
  }
1539
2032
 
1540
- await exportCurrentDataTableFormat(format);
2033
+ await exportCurrentDataTableFormat(format, filename);
1541
2034
  return;
1542
2035
  }
1543
2036
  case 'toggle-data-tables':
@@ -1715,6 +2208,7 @@ async function handleAction(actionNode) {
1715
2208
  }
1716
2209
  return;
1717
2210
  case 'reload-data-route':
2211
+ preserveDataRowEditorSelectionForReload();
1718
2212
  await refreshCurrentRoute();
1719
2213
  return;
1720
2214
  default:
@@ -1735,7 +2229,37 @@ function canApplyMediaTaggingShortcut(state) {
1735
2229
  }
1736
2230
 
1737
2231
  document.addEventListener('click', event => {
1738
- const actionNode = event.target.closest('[data-action]');
2232
+ const target = event.target instanceof Element ? event.target : null;
2233
+
2234
+ if (!target) {
2235
+ return;
2236
+ }
2237
+
2238
+ const copyColumnMenu = target.closest('[data-copy-column-menu]');
2239
+
2240
+ if (!copyColumnMenu) {
2241
+ closeCopyColumnMenus();
2242
+ } else if (target.closest('.query-result-column-menu__toggle')) {
2243
+ window.requestAnimationFrame(() => {
2244
+ if (copyColumnMenu instanceof HTMLDetailsElement && copyColumnMenu.open) {
2245
+ closeCopyColumnMenus(copyColumnMenu);
2246
+ }
2247
+ });
2248
+ }
2249
+
2250
+ const sidebarDatabasePicker = target.closest('.sidebar-db-picker');
2251
+
2252
+ if (!sidebarDatabasePicker) {
2253
+ closeSidebarDatabasePickers();
2254
+ } else if (target.closest('.sidebar-footer-card')) {
2255
+ window.requestAnimationFrame(() => {
2256
+ if (sidebarDatabasePicker instanceof HTMLDetailsElement && sidebarDatabasePicker.open) {
2257
+ closeSidebarDatabasePickers(sidebarDatabasePicker);
2258
+ }
2259
+ });
2260
+ }
2261
+
2262
+ const actionNode = target.closest('[data-action]');
1739
2263
 
1740
2264
  if (!actionNode) {
1741
2265
  return;
@@ -1744,6 +2268,18 @@ document.addEventListener('click', event => {
1744
2268
  handleAction(actionNode);
1745
2269
  });
1746
2270
 
2271
+ document.addEventListener('contextmenu', event => {
2272
+ const target = event.target instanceof Element ? event.target : null;
2273
+ const headerNode = target?.closest('[data-result-column-header]');
2274
+
2275
+ if (!headerNode) {
2276
+ return;
2277
+ }
2278
+
2279
+ event.preventDefault();
2280
+ openCopyColumnMenu(headerNode);
2281
+ });
2282
+
1747
2283
  document.addEventListener('keydown', event => {
1748
2284
  const target = event.target;
1749
2285
  const state = getState();
@@ -1810,6 +2346,18 @@ document.addEventListener('keydown', event => {
1810
2346
  return;
1811
2347
  }
1812
2348
 
2349
+ if (document.querySelector('[data-copy-column-menu][open]')) {
2350
+ event.preventDefault();
2351
+ closeCopyColumnMenus();
2352
+ return;
2353
+ }
2354
+
2355
+ if (document.querySelector('.sidebar-db-picker[open]')) {
2356
+ event.preventDefault();
2357
+ closeSidebarDatabasePickers();
2358
+ return;
2359
+ }
2360
+
1813
2361
  if (
1814
2362
  state.route.name === 'data' &&
1815
2363
  (typeof state.dataBrowser.selectedRowIndex === 'number' || Boolean(state.dataBrowser.selectedRow))
@@ -1826,12 +2374,33 @@ document.addEventListener('keydown', event => {
1826
2374
  });
1827
2375
 
1828
2376
  document.addEventListener('input', event => {
2377
+ const target = event.target instanceof Element ? event.target : null;
2378
+ const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
2379
+ const textCellInput = target?.closest('[data-row-editor-text-source]');
2380
+
2381
+ if (timestampInput) {
2382
+ syncRowEditorTimestampPreview(timestampInput);
2383
+ syncRowEditorFilePathPreview(timestampInput);
2384
+ }
2385
+
2386
+ if (textCellInput) {
2387
+ syncRowEditorCharacterCount(textCellInput);
2388
+ }
2389
+
1829
2390
  const bindNode = event.target.closest('[data-bind]');
1830
2391
 
1831
2392
  if (!bindNode) {
1832
2393
  return;
1833
2394
  }
1834
2395
 
2396
+ if (bindNode.dataset.bind === 'copy-column-format-field') {
2397
+ updateCopyColumnModalFormatField(
2398
+ bindNode.dataset.field,
2399
+ bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : bindNode.value,
2400
+ );
2401
+ return;
2402
+ }
2403
+
1835
2404
  if (bindNode.dataset.bind === 'current-query') {
1836
2405
  invalidateMainRenderCache();
1837
2406
  syncQueryEditorHighlight(bindNode);
@@ -1873,7 +2442,12 @@ document.addEventListener('input', event => {
1873
2442
  return;
1874
2443
  }
1875
2444
 
1876
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
2445
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
2446
+
2447
+ if (!syncTableDesignerDraftUi(bindNode)) {
2448
+ renderApp(getState());
2449
+ }
2450
+
1877
2451
  return;
1878
2452
  }
1879
2453
 
@@ -1882,7 +2456,14 @@ document.addEventListener('input', event => {
1882
2456
  return;
1883
2457
  }
1884
2458
 
1885
- updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
2459
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
2460
+ notify: false,
2461
+ });
2462
+
2463
+ if (!syncTableDesignerDraftUi(bindNode)) {
2464
+ renderApp(getState());
2465
+ }
2466
+
1886
2467
  return;
1887
2468
  }
1888
2469
 
@@ -1943,6 +2524,19 @@ document.addEventListener(
1943
2524
  );
1944
2525
 
1945
2526
  document.addEventListener('change', event => {
2527
+ const target = event.target instanceof Element ? event.target : null;
2528
+ const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
2529
+ const textCellInput = target?.closest('[data-row-editor-text-source]');
2530
+
2531
+ if (timestampInput) {
2532
+ syncRowEditorTimestampPreview(timestampInput);
2533
+ syncRowEditorFilePathPreview(timestampInput);
2534
+ }
2535
+
2536
+ if (textCellInput) {
2537
+ syncRowEditorCharacterCount(textCellInput);
2538
+ }
2539
+
1946
2540
  const bindNode = event.target.closest('[data-bind]');
1947
2541
 
1948
2542
  if (!bindNode) {
@@ -2001,7 +2595,12 @@ document.addEventListener('change', event => {
2001
2595
  return;
2002
2596
  }
2003
2597
 
2004
- updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
2598
+ updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
2599
+
2600
+ if (!syncTableDesignerDraftUi(bindNode)) {
2601
+ renderApp(getState());
2602
+ }
2603
+
2005
2604
  return;
2006
2605
  }
2007
2606
 
@@ -2018,7 +2617,14 @@ document.addEventListener('change', event => {
2018
2617
  return;
2019
2618
  }
2020
2619
 
2021
- updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value);
2620
+ updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
2621
+ notify: false,
2622
+ });
2623
+
2624
+ if (!syncTableDesignerDraftUi(bindNode)) {
2625
+ renderApp(getState());
2626
+ }
2627
+
2022
2628
  return;
2023
2629
  }
2024
2630
 
@@ -2027,6 +2633,16 @@ document.addEventListener('change', event => {
2027
2633
  return;
2028
2634
  }
2029
2635
 
2636
+ if (bindNode.dataset.bind === 'table-designer-constraint-field') {
2637
+ updateCurrentTableDesignerConstraintField(
2638
+ bindNode.dataset.constraintKind,
2639
+ bindNode.dataset.constraintId,
2640
+ bindNode.dataset.field,
2641
+ bindNode.value,
2642
+ );
2643
+ return;
2644
+ }
2645
+
2030
2646
  if (bindNode.dataset.bind === 'query-chart-draft:chartType') {
2031
2647
  updateCurrentQueryChartDraftField('chartType', bindNode.value);
2032
2648
  return;
@@ -2057,7 +2673,8 @@ document.addEventListener('submit', async event => {
2057
2673
  }
2058
2674
 
2059
2675
  event.preventDefault();
2060
- const formData = new FormData(form);
2676
+ const submitter = event.submitter instanceof HTMLElement ? event.submitter : null;
2677
+ const formData = submitter ? new FormData(form, submitter) : new FormData(form);
2061
2678
 
2062
2679
  switch (form.dataset.form) {
2063
2680
  case 'open-connection': {
@@ -2149,6 +2766,9 @@ document.addEventListener('submit', async event => {
2149
2766
  case 'create-media-tagging-mapping-table':
2150
2767
  await submitCreateMediaTaggingMappingTable();
2151
2768
  return;
2769
+ case 'copy-column':
2770
+ await submitCopyColumnModal(formData);
2771
+ return;
2152
2772
  case 'save-data-row': {
2153
2773
  const values = {};
2154
2774