sqlite-hub 0.10.0 → 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.
- package/README.md +44 -13
- package/database.sqlite +0 -0
- package/fill.js +526 -0
- package/frontend/js/api.js +70 -11
- package/frontend/js/app.js +104 -32
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +95 -0
- package/frontend/js/components/queryEditor.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +145 -6
- package/frontend/js/components/sidebar.js +5 -5
- package/frontend/js/store.js +303 -34
- package/frontend/js/views/data.js +94 -70
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +1 -1
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +1 -1
- package/server/routes/data.js +3 -0
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +25 -4
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +199 -1
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +76 -0
- package/tests/sql-identifier-safety.test.js +59 -0
package/frontend/js/api.js
CHANGED
|
@@ -366,6 +366,15 @@ export function getDataTable(tableName, options = {}) {
|
|
|
366
366
|
params.set("sortDirection", String(options.sortDirection));
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
const filterColumn = String(options.filterColumn ?? "").trim();
|
|
370
|
+
const filterValue = String(options.filterValue ?? "");
|
|
371
|
+
|
|
372
|
+
if (filterColumn && filterValue.trim()) {
|
|
373
|
+
params.set("filterColumn", filterColumn);
|
|
374
|
+
params.set("filterOperator", String(options.filterOperator ?? "="));
|
|
375
|
+
params.set("filterValue", filterValue);
|
|
376
|
+
}
|
|
377
|
+
|
|
369
378
|
const query = params.toString();
|
|
370
379
|
|
|
371
380
|
return request(`/api/data/${encodeURIComponent(tableName)}${query ? `?${query}` : ""}`);
|
|
@@ -410,22 +419,72 @@ export function patchSettings(settings) {
|
|
|
410
419
|
});
|
|
411
420
|
}
|
|
412
421
|
|
|
413
|
-
|
|
414
|
-
|
|
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}`, {
|
|
415
448
|
method: "POST",
|
|
416
449
|
body: { sql },
|
|
417
|
-
fallbackFilename:
|
|
450
|
+
fallbackFilename: `query-results.${extension}`,
|
|
418
451
|
});
|
|
419
452
|
}
|
|
420
453
|
|
|
421
|
-
export function
|
|
422
|
-
return
|
|
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", {
|
|
423
472
|
method: "POST",
|
|
424
|
-
body:
|
|
425
|
-
tableName,
|
|
426
|
-
sortColumn: options.sortColumn,
|
|
427
|
-
sortDirection: options.sortDirection,
|
|
428
|
-
},
|
|
429
|
-
fallbackFilename: `${tableName || "table"}.csv`,
|
|
473
|
+
body: buildTableExportBody(tableName, options),
|
|
430
474
|
});
|
|
431
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
|
+
}
|
package/frontend/js/app.js
CHANGED
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
dismissMediaTaggingIssue,
|
|
34
34
|
dismissToast,
|
|
35
35
|
executeCurrentQuery,
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
@@ -73,6 +77,7 @@ import {
|
|
|
73
77
|
toggleStructureTablesPanel,
|
|
74
78
|
setDataPage,
|
|
75
79
|
setDataPageSize,
|
|
80
|
+
setDataFilterOperator,
|
|
76
81
|
setDataSearchColumn,
|
|
77
82
|
setDataSearchQuery,
|
|
78
83
|
toggleDataTablesPanel,
|
|
@@ -131,10 +136,7 @@ import { renderOverviewView } from './views/overview.js';
|
|
|
131
136
|
import { renderSettingsView } from './views/settings.js';
|
|
132
137
|
import { renderStructureView } from './views/structure.js';
|
|
133
138
|
import { renderTableDesignerView } from './views/tableDesigner.js';
|
|
134
|
-
import {
|
|
135
|
-
replaceChildrenFromRenderedMarkup,
|
|
136
|
-
replaceElementFromRenderedMarkup,
|
|
137
|
-
} from './utils/dom.js';
|
|
139
|
+
import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
|
|
138
140
|
import { highlightSql } from './utils/format.js';
|
|
139
141
|
|
|
140
142
|
const appRoot = document.querySelector('#app');
|
|
@@ -332,7 +334,7 @@ function normalizeMediaTaggingRotationDegrees(value) {
|
|
|
332
334
|
return 0;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
return ((Math.round(numericValue / 90) * 90) % 360 + 360) % 360;
|
|
337
|
+
return (((Math.round(numericValue / 90) * 90) % 360) + 360) % 360;
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
function syncMediaTaggingMediaRotationUi(node, rotationDegrees) {
|
|
@@ -383,7 +385,9 @@ function syncMediaTaggingTagSearchUi(input) {
|
|
|
383
385
|
continue;
|
|
384
386
|
}
|
|
385
387
|
|
|
386
|
-
const searchText = String(tagOption.dataset.tagSearchText ?? '')
|
|
388
|
+
const searchText = String(tagOption.dataset.tagSearchText ?? '')
|
|
389
|
+
.trim()
|
|
390
|
+
.toLowerCase();
|
|
387
391
|
tagOption.hidden = Boolean(normalizedQuery) && !searchText.includes(normalizedQuery);
|
|
388
392
|
}
|
|
389
393
|
|
|
@@ -501,23 +505,18 @@ function syncQueryHistoryUi(historyId) {
|
|
|
501
505
|
return false;
|
|
502
506
|
}
|
|
503
507
|
|
|
504
|
-
const historyItem =
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
String(numericId),
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
)?.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');
|
|
512
515
|
|
|
513
516
|
if (historyItem && listItemNode instanceof HTMLElement) {
|
|
514
517
|
replaceElementFromRenderedMarkup(
|
|
515
518
|
listItemNode,
|
|
516
|
-
renderQueryHistoryListItem(
|
|
517
|
-
historyItem,
|
|
518
|
-
state.editor.historyActiveId,
|
|
519
|
-
state.editor.historySelectedId,
|
|
520
|
-
),
|
|
519
|
+
renderQueryHistoryListItem(historyItem, state.editor.historyActiveId, state.editor.historySelectedId),
|
|
521
520
|
);
|
|
522
521
|
}
|
|
523
522
|
|
|
@@ -1255,6 +1254,43 @@ async function executeEditorQueryAndNavigate() {
|
|
|
1255
1254
|
router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
|
|
1256
1255
|
}
|
|
1257
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
|
+
|
|
1258
1294
|
async function handleAction(actionNode) {
|
|
1259
1295
|
const { action } = actionNode.dataset;
|
|
1260
1296
|
|
|
@@ -1265,6 +1301,9 @@ async function handleAction(actionNode) {
|
|
|
1265
1301
|
case 'refresh-view':
|
|
1266
1302
|
await refreshCurrentRoute();
|
|
1267
1303
|
return;
|
|
1304
|
+
case 'open-row-editor-url':
|
|
1305
|
+
openRowEditorUrl(actionNode);
|
|
1306
|
+
return;
|
|
1268
1307
|
case 'open-modal':
|
|
1269
1308
|
openModal(actionNode.dataset.modal);
|
|
1270
1309
|
return;
|
|
@@ -1463,12 +1502,44 @@ async function handleAction(actionNode) {
|
|
|
1463
1502
|
router.navigate(tab === 'results' ? '/editor/results' : '/editor');
|
|
1464
1503
|
return;
|
|
1465
1504
|
}
|
|
1466
|
-
case '
|
|
1467
|
-
|
|
1505
|
+
case 'open-query-export-modal':
|
|
1506
|
+
openQueryExportModal();
|
|
1468
1507
|
return;
|
|
1469
|
-
case 'export-
|
|
1470
|
-
|
|
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);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
case 'open-data-export-modal':
|
|
1525
|
+
openDataExportModal();
|
|
1471
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
|
+
}
|
|
1472
1543
|
case 'toggle-data-tables':
|
|
1473
1544
|
toggleDataTablesPanel();
|
|
1474
1545
|
return;
|
|
@@ -1564,11 +1635,7 @@ async function handleAction(actionNode) {
|
|
|
1564
1635
|
const currentRotation = getState().mediaTagging.workflowMediaRotationDegrees ?? 0;
|
|
1565
1636
|
const command = actionNode.dataset.rotationCommand;
|
|
1566
1637
|
const nextRotation =
|
|
1567
|
-
command === 'left'
|
|
1568
|
-
? currentRotation - 90
|
|
1569
|
-
: command === 'right'
|
|
1570
|
-
? currentRotation + 90
|
|
1571
|
-
: 0;
|
|
1638
|
+
command === 'left' ? currentRotation - 90 : command === 'right' ? currentRotation + 90 : 0;
|
|
1572
1639
|
|
|
1573
1640
|
syncMediaTaggingMediaRotationUi(actionNode, nextRotation);
|
|
1574
1641
|
setMediaTaggingWorkflowMediaRotationDegrees(nextRotation, { notify: false });
|
|
@@ -1774,7 +1841,7 @@ document.addEventListener('input', event => {
|
|
|
1774
1841
|
}
|
|
1775
1842
|
|
|
1776
1843
|
if (bindNode.dataset.bind === 'data-search-query') {
|
|
1777
|
-
setDataSearchQuery(bindNode.value);
|
|
1844
|
+
void setDataSearchQuery(bindNode.value);
|
|
1778
1845
|
return;
|
|
1779
1846
|
}
|
|
1780
1847
|
|
|
@@ -1883,7 +1950,12 @@ document.addEventListener('change', event => {
|
|
|
1883
1950
|
}
|
|
1884
1951
|
|
|
1885
1952
|
if (bindNode.dataset.bind === 'data-search-column') {
|
|
1886
|
-
setDataSearchColumn(bindNode.value);
|
|
1953
|
+
void setDataSearchColumn(bindNode.value);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (bindNode.dataset.bind === 'data-filter-operator') {
|
|
1958
|
+
void setDataFilterOperator(bindNode.value);
|
|
1887
1959
|
return;
|
|
1888
1960
|
}
|
|
1889
1961
|
|
|
@@ -1,67 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
31
|
+
return `
|
|
38
32
|
<div class="flex flex-wrap justify-center gap-4">
|
|
39
33
|
${recentConnections
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
83
|
+
const hasActive = Boolean(activeConnection);
|
|
90
84
|
|
|
91
|
-
|
|
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 ?
|
|
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
|
-
|
|
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="
|
|
84
|
+
data-action="open-query-export-modal"
|
|
85
85
|
type="button"
|
|
86
86
|
>
|
|
87
|
-
|
|
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"
|