sqlite-hub 1.1.1 → 1.2.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 +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +201 -5
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +24 -15
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +53 -13
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +450 -38
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +204 -0
- package/frontend/js/views/charts.js +1 -1
- package/frontend/js/views/data.js +42 -16
- package/frontend/js/views/documents.js +184 -154
- package/frontend/js/views/settings.js +1 -0
- package/frontend/js/views/structure.js +47 -33
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/components.css +133 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -0
- package/frontend/styles/views.css +33 -21
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +70 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +37 -2
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +101 -0
- package/tests/email-preview.test.js +89 -0
- package/tests/markdown-documents.test.js +24 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +53 -0
- package/tests/settings-view.test.js +1 -0
- package/tests/structure-view.test.js +60 -0
package/frontend/js/app.js
CHANGED
|
@@ -24,6 +24,7 @@ import { renderTopNav } from './components/topNav.js';
|
|
|
24
24
|
import { createRouter } from './router.js';
|
|
25
25
|
import {
|
|
26
26
|
createActiveConnectionBackup,
|
|
27
|
+
downloadBackup,
|
|
27
28
|
createSettingsApiToken,
|
|
28
29
|
chooseOpenDatabasePath,
|
|
29
30
|
chooseCreateDatabasePath,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
exportCurrentQueryFormat,
|
|
46
47
|
getState,
|
|
47
48
|
initializeApp,
|
|
49
|
+
insertMarkdownIntoLastOpenDocument,
|
|
48
50
|
loadMoreQueryHistory,
|
|
49
51
|
openModal,
|
|
50
52
|
openOverviewInFinder,
|
|
@@ -53,6 +55,10 @@ import {
|
|
|
53
55
|
openDeleteEditorRowModal,
|
|
54
56
|
openDeleteQueryHistoryModal,
|
|
55
57
|
openDeleteSettingsApiTokenModal,
|
|
58
|
+
openCreateBackupModal,
|
|
59
|
+
openEditBackupNotesModal,
|
|
60
|
+
openDeleteBackupModal,
|
|
61
|
+
openRestoreBackupModal,
|
|
56
62
|
openDeleteQueryChartModal,
|
|
57
63
|
openDataExportModal,
|
|
58
64
|
openDeleteDocumentModal,
|
|
@@ -68,6 +74,7 @@ import {
|
|
|
68
74
|
openDataRowUpdatePreview,
|
|
69
75
|
openEditorRowUpdatePreview,
|
|
70
76
|
refreshCurrentRoute,
|
|
77
|
+
refreshBackups,
|
|
71
78
|
refreshMediaTaggingPreview,
|
|
72
79
|
removeConnection,
|
|
73
80
|
removeCurrentMediaTag,
|
|
@@ -105,6 +112,7 @@ import {
|
|
|
105
112
|
setEditorPanelVisibility,
|
|
106
113
|
setEditorTab,
|
|
107
114
|
submitDeleteChartConfirmation,
|
|
115
|
+
submitDeleteBackupConfirmation,
|
|
108
116
|
submitDeleteDocumentConfirmation,
|
|
109
117
|
submitDocumentInsertNote,
|
|
110
118
|
submitDocumentInsertTable,
|
|
@@ -112,6 +120,9 @@ import {
|
|
|
112
120
|
submitCreateMediaTaggingMappingTable,
|
|
113
121
|
submitDeleteQueryHistoryConfirmation,
|
|
114
122
|
submitDeleteSettingsApiTokenConfirmation,
|
|
123
|
+
submitBackupSafetyChoice,
|
|
124
|
+
submitCreateBackupConfirmation,
|
|
125
|
+
submitEditBackupNotesConfirmation,
|
|
115
126
|
submitRowUpdatePreviewConfirmation,
|
|
116
127
|
setQueryHistoryPanelVisibility,
|
|
117
128
|
sortDataTableByColumn,
|
|
@@ -129,6 +140,7 @@ import {
|
|
|
129
140
|
toggleChartsResultsPanel,
|
|
130
141
|
toggleChartsSqlPanel,
|
|
131
142
|
toggleCurrentDocumentTodo,
|
|
143
|
+
toggleDocumentsPanel,
|
|
132
144
|
toggleDocumentsPane,
|
|
133
145
|
setMediaTaggingWorkflowMediaDetailsVisible,
|
|
134
146
|
setMediaTaggingWorkflowMediaRotationDegrees,
|
|
@@ -159,6 +171,7 @@ import {
|
|
|
159
171
|
removeCurrentTableDesignerColumn,
|
|
160
172
|
} from './store.js';
|
|
161
173
|
import { renderChartsDetail, renderChartsView } from './views/charts.js';
|
|
174
|
+
import { renderBackupsView } from './views/backups.js';
|
|
162
175
|
import { renderConnectionsView } from './views/connections.js';
|
|
163
176
|
import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
|
|
164
177
|
import { renderDocumentsView } from './views/documents.js';
|
|
@@ -227,6 +240,7 @@ let lastRenderedPanelMarkup = '';
|
|
|
227
240
|
let lastRenderedModalMarkup = '';
|
|
228
241
|
let lastRenderedToastMarkup = '';
|
|
229
242
|
let lastRenderedChartsHistorySignature = '';
|
|
243
|
+
let lastRenderedChartsDetailSignature = '';
|
|
230
244
|
let lastRenderedChartsCardSignature = '';
|
|
231
245
|
let lastRenderedPanelOpen = false;
|
|
232
246
|
let lastRenderedLockedRoute = false;
|
|
@@ -241,6 +255,7 @@ const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
|
|
|
241
255
|
const APP_TITLE = 'SQLite Hub';
|
|
242
256
|
const ROUTE_TITLE_SEGMENTS = {
|
|
243
257
|
connections: 'Connections',
|
|
258
|
+
backups: 'Backups',
|
|
244
259
|
overview: 'Overview',
|
|
245
260
|
data: 'Data',
|
|
246
261
|
structure: 'Structure',
|
|
@@ -773,6 +788,27 @@ function buildChartsCardSignature(state) {
|
|
|
773
788
|
});
|
|
774
789
|
}
|
|
775
790
|
|
|
791
|
+
function buildChartsDetailSignature(state) {
|
|
792
|
+
if (state.route.name !== 'charts') {
|
|
793
|
+
return '';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const detail = state.charts.detail;
|
|
797
|
+
const historyVisible = state.charts.historyPanelVisible !== false || !state.charts.selectedHistoryId;
|
|
798
|
+
|
|
799
|
+
return JSON.stringify({
|
|
800
|
+
selectedHistoryId: state.charts.selectedHistoryId ?? null,
|
|
801
|
+
historyVisible,
|
|
802
|
+
detailLoading: Boolean(state.charts.detailLoading),
|
|
803
|
+
detailError: state.charts.detailError
|
|
804
|
+
? [state.charts.detailError.code ?? '', state.charts.detailError.message ?? '']
|
|
805
|
+
: null,
|
|
806
|
+
hasDetailItem: Boolean(detail?.item),
|
|
807
|
+
displayTitle: detail?.item?.displayTitle ?? '',
|
|
808
|
+
cards: buildChartsCardSignature(state),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
776
812
|
function syncChartsHistorySelectionUi(state) {
|
|
777
813
|
const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
|
|
778
814
|
const historyButtons = shellRefs.view.querySelectorAll(
|
|
@@ -1053,6 +1089,8 @@ function resolveView(state) {
|
|
|
1053
1089
|
return renderLandingView(state);
|
|
1054
1090
|
case 'connections':
|
|
1055
1091
|
return renderConnectionsView(state);
|
|
1092
|
+
case 'backups':
|
|
1093
|
+
return renderBackupsView(state);
|
|
1056
1094
|
case 'overview':
|
|
1057
1095
|
return renderOverviewView(state);
|
|
1058
1096
|
case 'charts':
|
|
@@ -1353,6 +1391,7 @@ function renderApp(state) {
|
|
|
1353
1391
|
const modalMarkup = renderModal(state);
|
|
1354
1392
|
const toastMarkup = renderToasts(state.toasts);
|
|
1355
1393
|
const chartsHistorySignature = buildChartsHistorySignature(state);
|
|
1394
|
+
const chartsDetailSignature = buildChartsDetailSignature(state);
|
|
1356
1395
|
const chartsCardSignature = buildChartsCardSignature(state);
|
|
1357
1396
|
const isLockedRoute = [
|
|
1358
1397
|
'editor',
|
|
@@ -1373,6 +1412,7 @@ function renderApp(state) {
|
|
|
1373
1412
|
const panelChanged = panel !== lastRenderedPanelMarkup;
|
|
1374
1413
|
const modalChanged = modalMarkup !== lastRenderedModalMarkup;
|
|
1375
1414
|
const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
|
|
1415
|
+
const chartsDetailChanged = chartsDetailSignature !== lastRenderedChartsDetailSignature;
|
|
1376
1416
|
const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
|
|
1377
1417
|
const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
|
|
1378
1418
|
const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
|
|
@@ -1421,12 +1461,17 @@ function renderApp(state) {
|
|
|
1421
1461
|
const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
|
|
1422
1462
|
|
|
1423
1463
|
if (historyPatched) {
|
|
1424
|
-
if (
|
|
1425
|
-
|
|
1426
|
-
|
|
1464
|
+
if (chartsDetailChanged) {
|
|
1465
|
+
if (chartsCardsChanged) {
|
|
1466
|
+
teardownQueryChartRenderer();
|
|
1467
|
+
}
|
|
1427
1468
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1469
|
+
preservedChartsDom = !chartsCardsChanged;
|
|
1470
|
+
mainPatched = patchChartsDetailUi(state, { preserveCharts: preservedChartsDom });
|
|
1471
|
+
} else {
|
|
1472
|
+
preservedChartsDom = true;
|
|
1473
|
+
mainPatched = syncChartsHistorySelectionUi(state);
|
|
1474
|
+
}
|
|
1430
1475
|
}
|
|
1431
1476
|
|
|
1432
1477
|
if (!mainPatched) {
|
|
@@ -1527,6 +1572,7 @@ function renderApp(state) {
|
|
|
1527
1572
|
lastRenderedModalMarkup = modalMarkup;
|
|
1528
1573
|
lastRenderedToastMarkup = toastMarkup;
|
|
1529
1574
|
lastRenderedChartsHistorySignature = chartsHistorySignature;
|
|
1575
|
+
lastRenderedChartsDetailSignature = chartsDetailSignature;
|
|
1530
1576
|
lastRenderedChartsCardSignature = chartsCardSignature;
|
|
1531
1577
|
lastRenderedPanelOpen = panelOpen;
|
|
1532
1578
|
lastRenderedLockedRoute = isLockedRoute;
|
|
@@ -1552,6 +1598,22 @@ async function executeEditorQueryAndNavigate() {
|
|
|
1552
1598
|
router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
|
|
1553
1599
|
}
|
|
1554
1600
|
|
|
1601
|
+
function quoteSqlIdentifier(identifier) {
|
|
1602
|
+
return `"${String(identifier ?? '').replace(/"/g, '""')}"`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async function openTableInSqlEditor(tableName) {
|
|
1606
|
+
const normalizedTableName = String(tableName ?? '').trim();
|
|
1607
|
+
|
|
1608
|
+
if (!normalizedTableName) {
|
|
1609
|
+
showToast('No table selected for SQL Editor.', 'alert');
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
setCurrentQuery(`SELECT * FROM ${quoteSqlIdentifier(normalizedTableName)};`);
|
|
1614
|
+
router.navigate('/editor');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1555
1617
|
function formatCurrentQuery() {
|
|
1556
1618
|
const currentQuery = getState().editor.sqlText ?? '';
|
|
1557
1619
|
const formattedQuery = formatSqlQuery(currentQuery);
|
|
@@ -1585,6 +1647,27 @@ function formatCurrentQuery() {
|
|
|
1585
1647
|
showToast('SQL query formatted.', 'success');
|
|
1586
1648
|
}
|
|
1587
1649
|
|
|
1650
|
+
async function copyCurrentQueryToClipboard() {
|
|
1651
|
+
const currentQuery = getState().editor.sqlText ?? '';
|
|
1652
|
+
|
|
1653
|
+
if (!currentQuery.trim()) {
|
|
1654
|
+
showToast('No SQL query to copy.', 'alert');
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (!navigator.clipboard?.writeText) {
|
|
1659
|
+
showToast('Clipboard API is not available.', 'alert');
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
try {
|
|
1664
|
+
await navigator.clipboard.writeText(currentQuery);
|
|
1665
|
+
showToast('SQL query copied.', 'success');
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
showToast('SQL query could not be copied.', 'alert');
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1588
1671
|
const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
1589
1672
|
|
|
1590
1673
|
function getOpenableUrl(value) {
|
|
@@ -1795,6 +1878,14 @@ function closeCopyColumnMenus(exceptMenu = null) {
|
|
|
1795
1878
|
});
|
|
1796
1879
|
}
|
|
1797
1880
|
|
|
1881
|
+
function closeDropdownButtons(exceptDropdown = null) {
|
|
1882
|
+
document.querySelectorAll('[data-dropdown-button][open]').forEach(dropdown => {
|
|
1883
|
+
if (dropdown !== exceptDropdown && dropdown instanceof HTMLDetailsElement) {
|
|
1884
|
+
dropdown.open = false;
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1798
1889
|
function closeSidebarDatabasePickers(exceptPicker = null) {
|
|
1799
1890
|
document.querySelectorAll('.sidebar-db-picker[open]').forEach(picker => {
|
|
1800
1891
|
if (picker !== exceptPicker && picker instanceof HTMLDetailsElement) {
|
|
@@ -2059,6 +2150,39 @@ function exportRowEditorJson() {
|
|
|
2059
2150
|
showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
|
|
2060
2151
|
}
|
|
2061
2152
|
|
|
2153
|
+
async function insertRowEditorJsonIntoDocument() {
|
|
2154
|
+
const payload = buildRowEditorJsonPayload(getState());
|
|
2155
|
+
|
|
2156
|
+
if (!payload) {
|
|
2157
|
+
showToast('No row is selected.', 'alert');
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const markdown = ['```json', payload.text, '```'].join('\n');
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
const result = await insertMarkdownIntoLastOpenDocument(markdown);
|
|
2165
|
+
|
|
2166
|
+
if (!result.documentId) {
|
|
2167
|
+
showToast('Open a document before inserting row JSON.', 'alert');
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
if (!result.inserted) {
|
|
2172
|
+
showToast('Row JSON could not be inserted.', 'alert');
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (!result.saved) {
|
|
2177
|
+
scheduleDocumentAutosave(result.documentId);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
showToast(`Row from "${payload.label}" inserted into document.`, 'success');
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
showToast('Open a document before inserting row JSON.', 'alert');
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2062
2186
|
function exportCurrentDocumentMarkdown() {
|
|
2063
2187
|
const documents = getState().documents;
|
|
2064
2188
|
|
|
@@ -2275,6 +2399,9 @@ async function handleAction(actionNode) {
|
|
|
2275
2399
|
case 'export-row-editor-json':
|
|
2276
2400
|
exportRowEditorJson();
|
|
2277
2401
|
return;
|
|
2402
|
+
case 'insert-row-editor-json-into-document':
|
|
2403
|
+
await insertRowEditorJsonIntoDocument();
|
|
2404
|
+
return;
|
|
2278
2405
|
case 'open-modal':
|
|
2279
2406
|
openModal(actionNode.dataset.modal, {
|
|
2280
2407
|
columnId: actionNode.dataset.columnId,
|
|
@@ -2446,6 +2573,36 @@ async function handleAction(actionNode) {
|
|
|
2446
2573
|
case 'create-backup':
|
|
2447
2574
|
await createActiveConnectionBackup();
|
|
2448
2575
|
return;
|
|
2576
|
+
case 'open-create-backup-modal':
|
|
2577
|
+
openCreateBackupModal();
|
|
2578
|
+
return;
|
|
2579
|
+
case 'refresh-backups':
|
|
2580
|
+
await refreshBackups();
|
|
2581
|
+
return;
|
|
2582
|
+
case 'open-table-in-sql-editor':
|
|
2583
|
+
await openTableInSqlEditor(actionNode.dataset.tableName);
|
|
2584
|
+
return;
|
|
2585
|
+
case 'open-restore-backup-modal':
|
|
2586
|
+
openRestoreBackupModal(actionNode.dataset.backupId);
|
|
2587
|
+
return;
|
|
2588
|
+
case 'open-edit-backup-notes-modal':
|
|
2589
|
+
openEditBackupNotesModal(actionNode.dataset.backupId);
|
|
2590
|
+
return;
|
|
2591
|
+
case 'open-delete-backup-modal':
|
|
2592
|
+
openDeleteBackupModal(actionNode.dataset.backupId);
|
|
2593
|
+
return;
|
|
2594
|
+
case 'download-backup':
|
|
2595
|
+
await downloadBackup(actionNode.dataset.backupId);
|
|
2596
|
+
return;
|
|
2597
|
+
case 'backup-safety-create':
|
|
2598
|
+
await submitBackupSafetyChoice('create');
|
|
2599
|
+
return;
|
|
2600
|
+
case 'backup-safety-continue':
|
|
2601
|
+
await submitBackupSafetyChoice('continue');
|
|
2602
|
+
return;
|
|
2603
|
+
case 'backup-safety-cancel':
|
|
2604
|
+
await submitBackupSafetyChoice('cancel');
|
|
2605
|
+
return;
|
|
2449
2606
|
case 'copy-media-tags': {
|
|
2450
2607
|
const tags = getState().mediaTagging.tags ?? [];
|
|
2451
2608
|
const formattedTags = tags
|
|
@@ -2476,6 +2633,9 @@ async function handleAction(actionNode) {
|
|
|
2476
2633
|
case 'format-current-query':
|
|
2477
2634
|
formatCurrentQuery();
|
|
2478
2635
|
return;
|
|
2636
|
+
case 'copy-current-query':
|
|
2637
|
+
await copyCurrentQueryToClipboard();
|
|
2638
|
+
return;
|
|
2479
2639
|
case 'delete-data-row':
|
|
2480
2640
|
openDeleteDataRowModal(actionNode.dataset.rowIndex);
|
|
2481
2641
|
return;
|
|
@@ -2659,6 +2819,9 @@ async function handleAction(actionNode) {
|
|
|
2659
2819
|
case 'toggle-table-designer-tables':
|
|
2660
2820
|
toggleTableDesignerTablesPanel();
|
|
2661
2821
|
return;
|
|
2822
|
+
case 'toggle-documents-panel':
|
|
2823
|
+
toggleDocumentsPanel();
|
|
2824
|
+
return;
|
|
2662
2825
|
case 'select-structure-entry':
|
|
2663
2826
|
if (actionNode.dataset.entryName) {
|
|
2664
2827
|
await selectStructureEntry(actionNode.dataset.entryName);
|
|
@@ -2867,6 +3030,20 @@ document.addEventListener('click', event => {
|
|
|
2867
3030
|
});
|
|
2868
3031
|
}
|
|
2869
3032
|
|
|
3033
|
+
const dropdownButton = target.closest('[data-dropdown-button]');
|
|
3034
|
+
|
|
3035
|
+
if (!dropdownButton) {
|
|
3036
|
+
closeDropdownButtons();
|
|
3037
|
+
} else if (target.closest('.dropdown-button__toggle')) {
|
|
3038
|
+
window.requestAnimationFrame(() => {
|
|
3039
|
+
if (dropdownButton instanceof HTMLDetailsElement && dropdownButton.open) {
|
|
3040
|
+
closeDropdownButtons(dropdownButton);
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
} else if (target.closest('.dropdown-button__item')) {
|
|
3044
|
+
closeDropdownButtons();
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2870
3047
|
const sidebarDatabasePicker = target.closest('.sidebar-db-picker');
|
|
2871
3048
|
|
|
2872
3049
|
if (!sidebarDatabasePicker) {
|
|
@@ -2986,6 +3163,12 @@ document.addEventListener('keydown', event => {
|
|
|
2986
3163
|
return;
|
|
2987
3164
|
}
|
|
2988
3165
|
|
|
3166
|
+
if (document.querySelector('[data-dropdown-button][open]')) {
|
|
3167
|
+
event.preventDefault();
|
|
3168
|
+
closeDropdownButtons();
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
2989
3172
|
if (document.querySelector('.sidebar-db-picker[open]')) {
|
|
2990
3173
|
event.preventDefault();
|
|
2991
3174
|
closeSidebarDatabasePickers();
|
|
@@ -3433,6 +3616,19 @@ document.addEventListener('submit', async event => {
|
|
|
3433
3616
|
case 'delete-row-confirm':
|
|
3434
3617
|
await submitDeleteRowConfirmation();
|
|
3435
3618
|
return;
|
|
3619
|
+
case 'create-backup':
|
|
3620
|
+
await submitCreateBackupConfirmation({
|
|
3621
|
+
name: String(formData.get('name') ?? ''),
|
|
3622
|
+
notes: String(formData.get('notes') ?? ''),
|
|
3623
|
+
type: String(formData.get('type') ?? 'manual'),
|
|
3624
|
+
});
|
|
3625
|
+
return;
|
|
3626
|
+
case 'edit-backup-notes':
|
|
3627
|
+
await submitEditBackupNotesConfirmation(String(formData.get('notes') ?? ''));
|
|
3628
|
+
return;
|
|
3629
|
+
case 'delete-backup-confirm':
|
|
3630
|
+
await submitDeleteBackupConfirmation();
|
|
3631
|
+
return;
|
|
3436
3632
|
case 'save-query-chart':
|
|
3437
3633
|
await saveCurrentQueryChartDraft();
|
|
3438
3634
|
return;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
function renderDataAttributes(attributes = {}) {
|
|
4
|
+
return Object.entries(attributes)
|
|
5
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
6
|
+
.map(([key, value]) => {
|
|
7
|
+
const attributeName = key
|
|
8
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
9
|
+
.replace(/_/g, "-")
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
|
|
12
|
+
return value === true
|
|
13
|
+
? `data-${attributeName}`
|
|
14
|
+
: `data-${attributeName}="${escapeHtml(value)}"`;
|
|
15
|
+
})
|
|
16
|
+
.join(" ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderDropdownItem(item = {}) {
|
|
20
|
+
const dataAttributes = renderDataAttributes(item.dataAttributes);
|
|
21
|
+
const actionAttribute = item.actionAttribute || "data-action";
|
|
22
|
+
const actionMarkup =
|
|
23
|
+
item.action === undefined || item.action === null
|
|
24
|
+
? ""
|
|
25
|
+
: `${escapeHtml(actionAttribute)}="${escapeHtml(item.action)}"`;
|
|
26
|
+
const disabled = item.disabled ? 'disabled aria-disabled="true"' : "";
|
|
27
|
+
const iconMarkup = item.icon
|
|
28
|
+
? `<span class="material-symbols-outlined">${escapeHtml(item.icon)}</span>`
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
return `
|
|
32
|
+
<button
|
|
33
|
+
class="dropdown-button__item ${item.danger ? "dropdown-button__item--danger" : ""}"
|
|
34
|
+
${actionMarkup}
|
|
35
|
+
${dataAttributes}
|
|
36
|
+
${disabled}
|
|
37
|
+
role="menuitem"
|
|
38
|
+
type="button"
|
|
39
|
+
>
|
|
40
|
+
${iconMarkup}
|
|
41
|
+
<span>${escapeHtml(item.label)}</span>
|
|
42
|
+
</button>
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderDropdownButton({
|
|
47
|
+
align = "right",
|
|
48
|
+
disabled = false,
|
|
49
|
+
icon = "more_horiz",
|
|
50
|
+
items = [],
|
|
51
|
+
label,
|
|
52
|
+
title = label,
|
|
53
|
+
} = {}) {
|
|
54
|
+
const iconMarkup = icon
|
|
55
|
+
? `<span class="material-symbols-outlined">${escapeHtml(icon)}</span>`
|
|
56
|
+
: "";
|
|
57
|
+
const buttonLabel = escapeHtml(label);
|
|
58
|
+
const chevron = '<span class="material-symbols-outlined dropdown-button__chevron">expand_more</span>';
|
|
59
|
+
|
|
60
|
+
if (disabled) {
|
|
61
|
+
return `
|
|
62
|
+
<button
|
|
63
|
+
aria-disabled="true"
|
|
64
|
+
class="standard-button dropdown-button__toggle"
|
|
65
|
+
disabled
|
|
66
|
+
title="${escapeHtml(title)}"
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
${iconMarkup}
|
|
70
|
+
${buttonLabel}
|
|
71
|
+
${chevron}
|
|
72
|
+
</button>
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `
|
|
77
|
+
<details class="dropdown-button dropdown-button--align-${escapeHtml(align)}" data-dropdown-button>
|
|
78
|
+
<summary
|
|
79
|
+
aria-label="${escapeHtml(title)}"
|
|
80
|
+
class="standard-button dropdown-button__toggle"
|
|
81
|
+
title="${escapeHtml(title)}"
|
|
82
|
+
>
|
|
83
|
+
${iconMarkup}
|
|
84
|
+
${buttonLabel}
|
|
85
|
+
${chevron}
|
|
86
|
+
</summary>
|
|
87
|
+
<div class="dropdown-button__panel" role="menu">
|
|
88
|
+
${items.map((item) => renderDropdownItem(item)).join("")}
|
|
89
|
+
</div>
|
|
90
|
+
</details>
|
|
91
|
+
`;
|
|
92
|
+
}
|