sqlite-hub 0.11.1 → 0.12.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.
@@ -419,22 +419,72 @@ export function patchSettings(settings) {
419
419
  });
420
420
  }
421
421
 
422
- export function downloadQueryCsv(sql) {
423
- return download("/api/export/query.csv", {
422
+ const TEXT_EXPORT_EXTENSIONS = {
423
+ csv: "csv",
424
+ tsv: "tsv",
425
+ md: "md",
426
+ };
427
+
428
+ function normalizeTextExportFormat(format) {
429
+ const normalized = String(format ?? "csv").toLowerCase();
430
+ return TEXT_EXPORT_EXTENSIONS[normalized] ? normalized : "csv";
431
+ }
432
+
433
+ export function getQueryExport(sql, format = "csv") {
434
+ return request("/api/export/query", {
435
+ method: "POST",
436
+ body: {
437
+ sql,
438
+ format: normalizeTextExportFormat(format),
439
+ },
440
+ });
441
+ }
442
+
443
+ export function downloadQueryExport(sql, format = "csv") {
444
+ const normalizedFormat = normalizeTextExportFormat(format);
445
+ const extension = TEXT_EXPORT_EXTENSIONS[normalizedFormat];
446
+
447
+ return download(`/api/export/query.${extension}`, {
424
448
  method: "POST",
425
449
  body: { sql },
426
- fallbackFilename: "query-results.csv",
450
+ fallbackFilename: `query-results.${extension}`,
427
451
  });
428
452
  }
429
453
 
430
- export function downloadTableCsv(tableName, options = {}) {
431
- return download("/api/export/table.csv", {
454
+ export function downloadQueryCsv(sql) {
455
+ return downloadQueryExport(sql, "csv");
456
+ }
457
+
458
+ function buildTableExportBody(tableName, options = {}) {
459
+ return {
460
+ tableName,
461
+ sortColumn: options.sortColumn,
462
+ sortDirection: options.sortDirection,
463
+ filterColumn: options.filterColumn,
464
+ filterOperator: options.filterOperator,
465
+ filterValue: options.filterValue,
466
+ format: normalizeTextExportFormat(options.format),
467
+ };
468
+ }
469
+
470
+ export function getTableExport(tableName, options = {}) {
471
+ return request("/api/export/table", {
432
472
  method: "POST",
433
- body: {
434
- tableName,
435
- sortColumn: options.sortColumn,
436
- sortDirection: options.sortDirection,
437
- },
438
- fallbackFilename: `${tableName || "table"}.csv`,
473
+ body: buildTableExportBody(tableName, options),
439
474
  });
440
475
  }
476
+
477
+ export function downloadTableExport(tableName, options = {}) {
478
+ const normalizedFormat = normalizeTextExportFormat(options.format);
479
+ const extension = TEXT_EXPORT_EXTENSIONS[normalizedFormat];
480
+
481
+ return download(`/api/export/table.${extension}`, {
482
+ method: "POST",
483
+ body: buildTableExportBody(tableName, { ...options, format: normalizedFormat }),
484
+ fallbackFilename: `${tableName || "table"}.${extension}`,
485
+ });
486
+ }
487
+
488
+ export function downloadTableCsv(tableName, options = {}) {
489
+ return downloadTableExport(tableName, { ...options, format: "csv" });
490
+ }
@@ -33,8 +33,10 @@ import {
33
33
  dismissMediaTaggingIssue,
34
34
  dismissToast,
35
35
  executeCurrentQuery,
36
- exportCurrentDataTableCsv,
37
- exportCurrentQueryCsv,
36
+ duplicateCurrentDataTableAsTable,
37
+ exportCurrentDataTableFormat,
38
+ duplicateCurrentQueryAsTable,
39
+ exportCurrentQueryFormat,
38
40
  getState,
39
41
  initializeApp,
40
42
  loadMoreQueryHistory,
@@ -45,6 +47,8 @@ import {
45
47
  openDeleteEditorRowModal,
46
48
  openDeleteQueryHistoryModal,
47
49
  openDeleteQueryChartModal,
50
+ openDataExportModal,
51
+ openQueryExportModal,
48
52
  openDataRowByIdentity,
49
53
  openEditConnectionModal,
50
54
  openCreateQueryChartModal,
@@ -132,10 +136,7 @@ import { renderOverviewView } from './views/overview.js';
132
136
  import { renderSettingsView } from './views/settings.js';
133
137
  import { renderStructureView } from './views/structure.js';
134
138
  import { renderTableDesignerView } from './views/tableDesigner.js';
135
- import {
136
- replaceChildrenFromRenderedMarkup,
137
- replaceElementFromRenderedMarkup,
138
- } from './utils/dom.js';
139
+ import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
139
140
  import { highlightSql } from './utils/format.js';
140
141
 
141
142
  const appRoot = document.querySelector('#app');
@@ -333,7 +334,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
333
334
  return 0;
334
335
  }
335
336
 
336
- return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
337
+ return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
337
338
  }
338
339
 
339
340
  function syncMediaTaggingMediaRotationUi(node, rotationDegrees) {
@@ -384,7 +385,9 @@ function syncMediaTaggingTagSearchUi(input) {
384
385
  continue;
385
386
  }
386
387
 
387
- const searchText = String(tagOption.dataset.tagSearchText ?? '').trim().toLowerCase();
388
+ const searchText = String(tagOption.dataset.tagSearchText ?? '')
389
+ .trim()
390
+ .toLowerCase();
388
391
  tagOption.hidden = Boolean(normalizedQuery) && !searchText.includes(normalizedQuery);
389
392
  }
390
393
 
@@ -502,23 +505,18 @@ function syncQueryHistoryUi(historyId) {
502
505
  return false;
503
506
  }
504
507
 
505
- const historyItem = state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
506
- const listItemNode = shellRefs.view.querySelector(
507
- [
508
- '[data-action="select-query-history-item"][data-history-id="',
509
- String(numericId),
510
- '"]',
511
- ].join(''),
512
- )?.closest('.query-history-item');
508
+ const historyItem =
509
+ state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
510
+ const listItemNode = shellRefs.view
511
+ .querySelector(
512
+ ['[data-action="select-query-history-item"][data-history-id="', String(numericId), '"]'].join(''),
513
+ )
514
+ ?.closest('.query-history-item');
513
515
 
514
516
  if (historyItem && listItemNode instanceof HTMLElement) {
515
517
  replaceElementFromRenderedMarkup(
516
518
  listItemNode,
517
- renderQueryHistoryListItem(
518
- historyItem,
519
- state.editor.historyActiveId,
520
- state.editor.historySelectedId,
521
- ),
519
+ renderQueryHistoryListItem(historyItem, state.editor.historyActiveId, state.editor.historySelectedId),
522
520
  );
523
521
  }
524
522
 
@@ -1256,6 +1254,43 @@ async function executeEditorQueryAndNavigate() {
1256
1254
  router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
1257
1255
  }
1258
1256
 
1257
+ const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
1258
+
1259
+ function getOpenableUrl(value) {
1260
+ const text = String(value ?? '').trim();
1261
+
1262
+ if (!OPENABLE_URL_PATTERN.test(text)) {
1263
+ return null;
1264
+ }
1265
+
1266
+ try {
1267
+ const url = new URL(text);
1268
+ return ['http:', 'https:'].includes(url.protocol) ? url.href : null;
1269
+ } catch (error) {
1270
+ return null;
1271
+ }
1272
+ }
1273
+
1274
+ function openRowEditorUrl(actionNode) {
1275
+ const field = actionNode.closest('[data-row-editor-url-field]');
1276
+ const inputValue = field?.querySelector('[data-row-editor-url-input]')?.value;
1277
+ const url = getOpenableUrl(inputValue ?? actionNode.dataset.url);
1278
+
1279
+ if (!url) {
1280
+ showToast('Field value is not a valid URL.', 'alert');
1281
+ return;
1282
+ }
1283
+
1284
+ const link = document.createElement('a');
1285
+
1286
+ link.href = url;
1287
+ link.target = '_blank';
1288
+ link.rel = 'noopener noreferrer';
1289
+ document.body.appendChild(link);
1290
+ link.click();
1291
+ link.remove();
1292
+ }
1293
+
1259
1294
  async function handleAction(actionNode) {
1260
1295
  const { action } = actionNode.dataset;
1261
1296
 
@@ -1266,6 +1301,9 @@ async function handleAction(actionNode) {
1266
1301
  case 'refresh-view':
1267
1302
  await refreshCurrentRoute();
1268
1303
  return;
1304
+ case 'open-row-editor-url':
1305
+ openRowEditorUrl(actionNode);
1306
+ return;
1269
1307
  case 'open-modal':
1270
1308
  openModal(actionNode.dataset.modal);
1271
1309
  return;
@@ -1464,12 +1502,44 @@ async function handleAction(actionNode) {
1464
1502
  router.navigate(tab === 'results' ? '/editor/results' : '/editor');
1465
1503
  return;
1466
1504
  }
1467
- case 'export-query-csv':
1468
- await exportCurrentQueryCsv();
1505
+ case 'open-query-export-modal':
1506
+ openQueryExportModal();
1507
+ return;
1508
+ case 'export-query-format': {
1509
+ const format = actionNode.dataset.exportFormat;
1510
+
1511
+ if (format === 'table') {
1512
+ const imported = await duplicateCurrentQueryAsTable();
1513
+
1514
+ if (imported) {
1515
+ router.navigate('/table-designer/new');
1516
+ }
1517
+
1518
+ return;
1519
+ }
1520
+
1521
+ await exportCurrentQueryFormat(format);
1469
1522
  return;
1470
- case 'export-data-csv':
1471
- await exportCurrentDataTableCsv();
1523
+ }
1524
+ case 'open-data-export-modal':
1525
+ openDataExportModal();
1472
1526
  return;
1527
+ case 'export-data-format': {
1528
+ const format = actionNode.dataset.exportFormat;
1529
+
1530
+ if (format === 'table') {
1531
+ const imported = await duplicateCurrentDataTableAsTable();
1532
+
1533
+ if (imported) {
1534
+ router.navigate('/table-designer/new');
1535
+ }
1536
+
1537
+ return;
1538
+ }
1539
+
1540
+ await exportCurrentDataTableFormat(format);
1541
+ return;
1542
+ }
1473
1543
  case 'toggle-data-tables':
1474
1544
  toggleDataTablesPanel();
1475
1545
  return;
@@ -1565,11 +1635,7 @@ async function handleAction(actionNode) {
1565
1635
  const currentRotation = getState().mediaTagging.workflowMediaRotationDegrees ?? 0;
1566
1636
  const command = actionNode.dataset.rotationCommand;
1567
1637
  const nextRotation =
1568
- command === 'left'
1569
- ? currentRotation - 90
1570
- : command === 'right'
1571
- ? currentRotation + 90
1572
- : 0;
1638
+ command === 'left' ? currentRotation - 90 : command === 'right' ? currentRotation + 90 : 0;
1573
1639
 
1574
1640
  syncMediaTaggingMediaRotationUi(actionNode, nextRotation);
1575
1641
  setMediaTaggingWorkflowMediaRotationDegrees(nextRotation, { notify: false });
@@ -1,67 +1,61 @@
1
- import {
2
- escapeHtml,
3
- formatBytes,
4
- formatDateTime,
5
- truncateMiddle,
6
- } from "../utils/format.js";
7
- import { renderConnectionLogo } from "./connectionLogo.js";
1
+ import { escapeHtml, formatBytes, formatDateTime, truncateMiddle } from '../utils/format.js';
2
+ import { renderConnectionLogo } from './connectionLogo.js';
8
3
 
9
4
  function renderRecentConnectionButton(connection) {
10
- return [
11
- '<button class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high" data-action="select-connection" data-connection-id="',
12
- escapeHtml(connection.id),
13
- '" type="button">',
14
- renderConnectionLogo(connection, {
15
- containerClass:
16
- "flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
17
- imageClassName: "h-full w-full object-cover",
18
- iconClassName: "text-sm text-primary-container",
19
- }),
20
- '<span class="min-w-0"><span class="block truncate font-mono text-xs">',
21
- escapeHtml(connection.label),
22
- '</span><span class="block truncate text-[10px] text-on-surface-variant/45">',
23
- escapeHtml(truncateMiddle(connection.path, 34)),
24
- "</span></span></button>",
25
- ].join("");
5
+ return [
6
+ '<button class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high" data-action="select-connection" data-connection-id="',
7
+ escapeHtml(connection.id),
8
+ '" type="button">',
9
+ renderConnectionLogo(connection, {
10
+ containerClass: 'flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest',
11
+ imageClassName: 'h-full w-full object-cover',
12
+ iconClassName: 'text-sm text-primary-container',
13
+ }),
14
+ '<span class="min-w-0"><span class="block truncate font-mono text-xs">',
15
+ escapeHtml(connection.label),
16
+ '</span><span class="block truncate text-[10px] text-on-surface-variant/45">',
17
+ escapeHtml(truncateMiddle(connection.path, 34)),
18
+ '</span></span></button>',
19
+ ].join('');
26
20
  }
27
21
 
28
22
  function renderRecentConnections(recentConnections = []) {
29
- if (!recentConnections.length) {
30
- return `
23
+ if (!recentConnections.length) {
24
+ return `
31
25
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/40">
32
26
  No recent SQLite databases recorded yet.
33
27
  </div>
34
28
  `;
35
- }
29
+ }
36
30
 
37
- return `
31
+ return `
38
32
  <div class="flex flex-wrap justify-center gap-4">
39
33
  ${recentConnections
40
- .slice(0, 4)
41
- .map((connection) => renderRecentConnectionButton(connection))
42
- .join("")}
34
+ .slice(0, 4)
35
+ .map(connection => renderRecentConnectionButton(connection))
36
+ .join('')}
43
37
  </div>
44
38
  `;
45
39
  }
46
40
 
47
41
  function renderActiveConnection(activeConnection) {
48
- if (!activeConnection) {
49
- return `
42
+ if (!activeConnection) {
43
+ return `
50
44
  <p class="font-light text-lg uppercase tracking-wide text-on-surface-variant">
51
45
  No database connected
52
46
  </p>
53
47
  `;
54
- }
48
+ }
55
49
 
56
- return `
50
+ return `
57
51
  <div class="mx-auto max-w-2xl border border-outline-variant/15 bg-surface-container-low px-6 py-5 text-left">
58
52
  <div class="flex flex-wrap items-start justify-between gap-4">
59
53
  <div class="flex items-start gap-4">
60
54
  ${renderConnectionLogo(activeConnection, {
61
- containerClass:
62
- "flex h-14 w-14 items-center justify-center overflow-hidden border border-outline-variant/15 bg-surface-container-highest",
63
- imageClassName: "h-full w-full object-cover",
64
- iconClassName: "text-2xl text-primary-container",
55
+ containerClass:
56
+ 'flex h-14 w-14 items-center justify-center overflow-hidden border border-outline-variant/15 bg-surface-container-highest',
57
+ imageClassName: 'h-full w-full object-cover',
58
+ iconClassName: 'text-2xl text-primary-container',
65
59
  })}
66
60
  <div>
67
61
  <p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
@@ -71,14 +65,14 @@ function renderActiveConnection(activeConnection) {
71
65
  ${escapeHtml(activeConnection.label)}
72
66
  </h2>
73
67
  <p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
74
- truncateMiddle(activeConnection.path, 72)
68
+ truncateMiddle(activeConnection.path, 72),
75
69
  )}</p>
76
70
  </div>
77
71
  </div>
78
72
  <div class="text-right text-xs text-on-surface-variant/65">
79
73
  <div>${escapeHtml(formatBytes(activeConnection.sizeBytes))}</div>
80
74
  <div class="mt-1">${escapeHtml(formatDateTime(activeConnection.lastModifiedAt))}</div>
81
- <div class="mt-1">${activeConnection.readOnly ? "READ_ONLY" : "READ_WRITE"}</div>
75
+ <div class="mt-1">${activeConnection.readOnly ? 'READ_ONLY' : 'READ_WRITE'}</div>
82
76
  </div>
83
77
  </div>
84
78
  </div>
@@ -86,9 +80,9 @@ function renderActiveConnection(activeConnection) {
86
80
  }
87
81
 
88
82
  export function renderEmptyState({ activeConnection, recentConnections = [] }) {
89
- const hasActive = Boolean(activeConnection);
83
+ const hasActive = Boolean(activeConnection);
90
84
 
91
- return `
85
+ return `
92
86
  <section class="landing-view machined-grid px-6">
93
87
  <div class="landing-accent landing-accent--a"></div>
94
88
  <div class="landing-accent landing-accent--b"></div>
@@ -96,12 +90,14 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
96
90
  <div class="empty-state-shell z-10 text-center">
97
91
  <div class="mb-2">
98
92
  <span class="font-mono text-[10px] tracking-[0.3em] text-primary-container/40">
99
- SYSTEM_READY // ${hasActive ? "ACTIVE_CONTEXT" : "IDLE_STATE"}
93
+ SYSTEM_READY // ${hasActive ? 'ACTIVE_CONTEXT' : 'IDLE_STATE'}
100
94
  </span>
101
95
  </div>
102
96
  <h1 class="mb-4 font-headline text-7xl font-black tracking-tighter text-primary-container opacity-90 md:text-9xl">
103
97
  SQLite Hub
104
98
  </h1>
99
+ <p class="font-mono text-primary-container/40">The only fucking SQLite Manager you'll ever need.</p>
100
+ <br/>
105
101
  <div class="mx-auto mb-12 max-w-3xl space-y-4">
106
102
  ${renderActiveConnection(activeConnection)}
107
103
  <div class="h-[2px] w-12 bg-primary-container mx-auto"></div>
@@ -128,8 +124,8 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
128
124
  </button>
129
125
  </div>
130
126
  ${
131
- hasActive
132
- ? `
127
+ hasActive
128
+ ? `
133
129
  <div class="mx-auto mt-8 grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-3">
134
130
  <button
135
131
  class="standard-button"
@@ -157,7 +153,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
157
153
  </button>
158
154
  </div>
159
155
  `
160
- : ""
156
+ : ''
161
157
  }
162
158
  <div class="mt-16 flex flex-col items-center gap-4 opacity-70 transition-opacity hover:opacity-100">
163
159
  <p class="font-mono text-[10px] tracking-widest text-on-surface-variant">RECENT_TARGETS</p>
@@ -800,6 +800,91 @@ function renderDeleteQueryHistoryForm(modal) {
800
800
  ].join("");
801
801
  }
802
802
 
803
+ function renderQueryExportPreview(lines = []) {
804
+ return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
805
+ }
806
+
807
+ function getExportOptions() {
808
+ return [
809
+ { label: "CSV", format: "csv", icon: "table_rows", meta: ".csv", preview: ["id,name", "1,Acme"] },
810
+ { label: "TSV", format: "tsv", icon: "view_column", meta: ".tsv", preview: ["id\tname", "1\tAcme"] },
811
+ {
812
+ label: "Markdown",
813
+ format: "md",
814
+ icon: "article",
815
+ meta: ".md",
816
+ preview: ["| id | name |", "| --- | --- |"],
817
+ },
818
+ {
819
+ label: "Duplicate",
820
+ format: "table",
821
+ icon: "table_chart",
822
+ meta: "as table",
823
+ preview: ["columns inferred", "rows prefilled"],
824
+ },
825
+ ];
826
+ }
827
+
828
+ function renderTextExportModal(modal, action) {
829
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
830
+
831
+ return `
832
+ <div class="space-y-5">
833
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
834
+ ${getExportOptions()
835
+ .map(
836
+ (option) => `
837
+ <button
838
+ class="group flex min-h-36 flex-col justify-between border border-outline-variant/20 bg-surface-container-low px-5 py-5 text-left text-on-surface transition-colors hover:border-primary-container/45 hover:bg-surface-container-high focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary-container disabled:cursor-wait disabled:opacity-60"
839
+ data-action="${escapeHtml(action)}"
840
+ data-export-format="${escapeHtml(option.format)}"
841
+ type="button"
842
+ ${disabledAttribute}
843
+ >
844
+ <span class="flex w-full items-start justify-between gap-4">
845
+ <span class="flex min-w-0 items-center gap-3">
846
+ <span class="flex h-10 w-10 shrink-0 items-center justify-center border border-outline-variant/20 bg-surface-container-highest text-primary-container group-hover:border-primary-container/35">
847
+ <span class="material-symbols-outlined text-xl">${escapeHtml(option.icon)}</span>
848
+ </span>
849
+ <span class="min-w-0">
850
+ <span class="block truncate font-headline text-lg font-black uppercase tracking-normal text-primary-container">
851
+ ${escapeHtml(option.label)}
852
+ </span>
853
+ <span class="mt-1 block font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
854
+ ${escapeHtml(option.meta)}
855
+ </span>
856
+ </span>
857
+ </span>
858
+ <span class="material-symbols-outlined mt-1 text-base text-on-surface-variant/35 group-hover:text-primary-container">
859
+ arrow_forward
860
+ </span>
861
+ </span>
862
+ <span class="mt-5 block w-full overflow-hidden border border-outline-variant/10 bg-surface-container-lowest px-3 py-3 font-mono text-[11px] leading-5 text-on-surface-variant/70">
863
+ ${renderQueryExportPreview(option.preview)}
864
+ </span>
865
+ </button>
866
+ `,
867
+ )
868
+ .join("")}
869
+ </div>
870
+ ${renderError(modal.error)}
871
+ <div class="flex justify-end">
872
+ <button class="standard-button" data-action="close-modal" type="button">
873
+ Cancel
874
+ </button>
875
+ </div>
876
+ </div>
877
+ `;
878
+ }
879
+
880
+ function renderQueryExportModal(modal) {
881
+ return renderTextExportModal(modal, "export-query-format");
882
+ }
883
+
884
+ function renderDataExportModal(modal) {
885
+ return renderTextExportModal(modal, "export-data-format");
886
+ }
887
+
803
888
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
804
889
  const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
805
890
  const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
@@ -981,6 +1066,16 @@ export function renderModal(state) {
981
1066
  title: "Delete Query",
982
1067
  body: renderDeleteQueryHistoryForm(modal),
983
1068
  },
1069
+ "query-export": {
1070
+ eyebrow: "SQL Editor // Export query result",
1071
+ title: "Export Query",
1072
+ body: renderQueryExportModal(modal),
1073
+ },
1074
+ "data-export": {
1075
+ eyebrow: "Data Browser // Export table data",
1076
+ title: "Export Table",
1077
+ body: renderDataExportModal(modal),
1078
+ },
984
1079
  "create-media-tagging-tag-table": {
985
1080
  eyebrow: "Media Tagging // Create default tag table",
986
1081
  title: "Create Tag Table",
@@ -81,10 +81,11 @@ export function renderQueryEditor({
81
81
  </button>
82
82
  <button
83
83
  class="${secondaryButtonClass}"
84
- data-action="export-query-csv"
84
+ data-action="open-query-export-modal"
85
85
  type="button"
86
86
  >
87
- ${exporting ? "Exporting..." : "Export CSV"}
87
+ <span class="material-symbols-outlined text-sm">download</span>
88
+ ${exporting ? "Exporting..." : "Export"}
88
89
  </button>
89
90
  <button
90
91
  class="signature-button"