sqlite-hub 1.1.0 → 1.1.2

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 (58) hide show
  1. package/README.md +17 -196
  2. package/bin/sqlite-hub.js +7 -2
  3. package/frontend/js/api.js +60 -0
  4. package/frontend/js/app.js +140 -10
  5. package/frontend/js/components/dropdownButton.js +92 -0
  6. package/frontend/js/components/modal.js +991 -876
  7. package/frontend/js/components/queryEditor.js +29 -18
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/components/structureGraph.js +1 -0
  13. package/frontend/js/components/tableDesignerEditor.js +23 -42
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/router.js +2 -0
  17. package/frontend/js/store.js +407 -38
  18. package/frontend/js/utils/riskySql.js +165 -0
  19. package/frontend/js/views/backups.js +176 -0
  20. package/frontend/js/views/charts.js +12 -11
  21. package/frontend/js/views/data.js +37 -35
  22. package/frontend/js/views/documents.js +231 -162
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/structure.js +47 -43
  25. package/frontend/js/views/tableDesigner.js +45 -1
  26. package/frontend/styles/components.css +237 -59
  27. package/frontend/styles/structure-graph.css +10 -2
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +2 -0
  30. package/frontend/styles/views.css +84 -41
  31. package/package.json +2 -1
  32. package/server/routes/backups.js +120 -0
  33. package/server/routes/connections.js +26 -3
  34. package/server/routes/externalApi.js +6 -2
  35. package/server/server.js +3 -1
  36. package/server/services/databaseCommandService.js +4 -3
  37. package/server/services/sqlite/backupService.js +497 -66
  38. package/server/services/sqlite/connectionManager.js +2 -2
  39. package/server/services/sqlite/dataBrowserService.js +11 -3
  40. package/server/services/sqlite/importService.js +25 -0
  41. package/server/services/sqlite/sqlExecutor.js +2 -0
  42. package/server/services/storage/appStateStore.js +379 -88
  43. package/tests/api-token-auth.test.js +45 -2
  44. package/tests/backup-manager.test.js +140 -0
  45. package/tests/backups-view.test.js +64 -0
  46. package/tests/charts-height-preset-storage.test.js +60 -0
  47. package/tests/charts-route-state.test.js +144 -0
  48. package/tests/cli-service-delegation.test.js +97 -0
  49. package/tests/connection-removal.test.js +52 -0
  50. package/tests/database-command-service.test.js +38 -3
  51. package/tests/documents-view.test.js +132 -0
  52. package/tests/dropdown-button.test.js +75 -0
  53. package/tests/query-editor.test.js +28 -0
  54. package/tests/query-history-detail.test.js +37 -0
  55. package/tests/query-history-header.test.js +30 -0
  56. package/tests/risky-sql.test.js +30 -0
  57. package/tests/row-editor-null-values.test.js +24 -0
  58. package/tests/structure-view.test.js +56 -0
package/README.md CHANGED
@@ -23,7 +23,7 @@ SQLite Hub keeps that workflow sharp:
23
23
  - copy result columns with formatting, headers, first-10 previews, TXT export, and Markdown todo export
24
24
  - keep database-scoped Markdown documents with previews, autosave, imports, exports, and saved-query inserts
25
25
  - switch between recent databases with sidebar quick picks
26
- - create simple local backups of the active database
26
+ - create verified local backups of the active database and get safety prompts before risky operations
27
27
  - run and format SQL in a syntax-highlighted editor with history, messages, and performance metrics
28
28
  - keep large interactive query results bounded while full exports remain available
29
29
  - turn query-history results into local charts
@@ -176,9 +176,18 @@ The Settings view reports the installed SQLite Hub version and the actual SQLite
176
176
 
177
177
  The active database footer in the sidebar opens a quick-pick panel with the five most recent databases, so you can switch databases without going back to the Connections view.
178
178
 
179
- ### Simple backups
179
+ ### Backup Manager
180
180
 
181
- Create timestamped local backups of the active SQLite database in one click. Backups are stored as plain file copies in a local `backups` folder next to the database.
181
+ Create verified local backups of the active SQLite database, review backup metadata, edit backup notes, download backup files, restore verified backups, and delete managed backups from the Backups view. SQLite Hub stores backup files under its local app-state backup directory by connection id and keeps a `manifest.json` beside each database's backup files. Each backup is created through SQLite's backup API, hashed with SHA-256, and verified with `PRAGMA quick_check` before it is marked as verified.
182
+
183
+ SQLite Hub also proposes a safety backup before operations that can be hard to undo:
184
+
185
+ - SQL Editor execution when the statement set contains `DROP TABLE`, `ALTER TABLE`, `CREATE TABLE`, `CREATE INDEX`, `CREATE VIEW`, `CREATE TRIGGER`, `DROP INDEX`, `DROP VIEW`, `DROP TRIGGER`, `REINDEX`, or `VACUUM`.
186
+ - SQL Editor execution with multiple schema-affecting statements in one run; this is treated as a migration and suggests a `Before migration` backup.
187
+ - SQL import into the currently active database when the dump is larger than 5 MB or contains more than 1,000 parsed SQL statements.
188
+ - Restore from a managed backup, because the current active database file will be replaced.
189
+
190
+ The safety dialog lets you create the backup and continue, continue without creating one, or cancel the operation.
182
191
 
183
192
  ### Local-first
184
193
 
@@ -205,200 +214,12 @@ npm install -g sqlite-hub
205
214
  sqlite-hub --port:4174
206
215
  ```
207
216
 
208
- ## CLI Interface
209
-
210
- SQLite Hub ships with a built-in CLI that lets you start the app or query information about your imported databases directly from the terminal.
211
-
212
- ### Start the app
213
-
214
- ```bash
215
- sqlite-hub # start on default port 4173
216
- sqlite-hub --port:4174 # start on a custom port
217
- sqlite-hub --open # open SQLite Hub in the browser
218
- sqlite-hub --info # show port, URL, versions, and update status
219
- sqlite-hub --help # show help text
220
- sqlite-hub --version # show version number
221
- ```
222
-
223
- ### List all imported databases
224
-
225
- ```bash
226
- sqlite-hub --database
227
- sqlite-hub -d
228
- ```
229
-
230
- Shows an overview of all databases that have been opened in SQLite Hub, including:
231
-
232
- - database name/label
233
- - file path
234
- - file size
235
- - last opened timestamp
236
- - read-only status
237
-
238
- ### Query specific database details
239
-
240
- Retrieve details about a single database by its name (case-insensitive):
241
-
242
- ```bash
243
- sqlite-hub --database:Billly --path # get the file path
244
- sqlite-hub --database:Billly --size # get the file size
245
- sqlite-hub --database:Billly --lastopened # get last opened timestamp
246
- ```
247
-
248
- ### List all tables in a database
249
-
250
- ```bash
251
- sqlite-hub --database:Billly --tables
252
- ```
253
-
254
- Opens the database in read-only mode and prints all table names, sorted alphabetically.
255
-
256
- ### Inspect a table
257
-
258
- ```bash
259
- sqlite-hub --database:Billly --table:companies
260
- ```
261
-
262
- Prints table metadata such as columns, primary keys, foreign keys, indexes, row count, and row identity strategy.
263
-
264
- ### SQL Editor - Raw SQL And Saved Queries
265
-
266
- Execute raw SQL through the same SQL Editor execution path used by the app:
267
-
268
- ```bash
269
- sqlite-hub --database:Unit-00 --query:"SELECT * FROM companies LIMIT 10"
270
- sqlite-hub --database:Unit-00 --query:"SELECT * FROM companies LIMIT 10" --store:"Company Sample"
271
- ```
272
-
273
- Raw CLI queries are recorded in Query History. Add `--store:"name"` to title the
274
- history item and mark it as saved. Raw SQL execution is rejected when the target
275
- database is marked read-only.
276
-
277
- List all saved queries for a database:
278
-
279
- ```bash
280
- sqlite-hub --database:Unit-00 --queries
281
- ```
282
-
283
- Execute a specific saved query by name:
284
-
285
- ```bash
286
- sqlite-hub --database:Unit-00 --execute:"15min Posting Buckets without id 96"
287
- ```
288
-
289
- This searches the query history for the given database, finds the matching saved query by title, executes it, and returns all results with metadata (row count, columns, timing, and data).
290
-
291
- Show the saved query SQL without executing it:
292
-
293
- ```bash
294
- sqlite-hub --database:Unit-00 --saved-query:"Stock Winners"
295
- ```
296
-
297
- Show the saved notes for a query:
298
-
299
- ```bash
300
- sqlite-hub --database:Unit-00 --notes:"TOP25 Loser and Winner EOD, T1, T3, T5"
301
- ```
302
-
303
- Export a saved query using the same CSV, TSV, Markdown, and JSON export logic as the SQL Editor:
304
-
305
- ```bash
306
- sqlite-hub --database:Unit-00 --export:"Stock Winners" --format:csv
307
- sqlite-hub --database:Unit-00 --export:"Stock Winners" --format:tsv
308
- sqlite-hub --database:Unit-00 --export:"Stock Winners" --format:md
309
- sqlite-hub --database:Unit-00 --export:"Stock Winners" --format:json
310
- ```
311
-
312
- The export is written to the current working directory using the generated query export filename.
217
+ ## CLI
313
218
 
314
- ### Documents CLI
315
-
316
- List all Markdown documents stored for a database:
317
-
318
- ```bash
319
- sqlite-hub --database:Unit-00 --documents
320
- ```
321
-
322
- Print one document's Markdown content:
323
-
324
- ```bash
325
- sqlite-hub --database:Unit-00 --documents:"Research Notes"
326
- ```
327
-
328
- Export one document as a `.md` file into the current working directory:
329
-
330
- ```bash
331
- sqlite-hub --database:Unit-00 --documents:"Research Notes" --export
332
- sqlite-hub --database:Unit-00 --documents:"Research Notes--export"
333
- ```
334
-
335
- Documents can be matched by id, filename, title, or a partial filename/title match.
336
-
337
- ### Row JSON export
338
-
339
- Export a single row as JSON by primary key or rowid, using the same row-shaping logic as the Row Editor:
340
-
341
- ```bash
342
- sqlite-hub --database:Unit-00 --table:companies --export:0a754aba373d34972998792a0be4333c
343
- ```
344
-
345
- ### Available flags
346
-
347
- | Flag | Description |
348
- | --------------------------------------------------------------- | ----------------------------------------------- |
349
- | `--help`, `-h` | Show help text |
350
- | `--version`, `-v` | Show version number |
351
- | `--info` | Show port, URL, versions, and update status |
352
- | `--open` | Open SQLite Hub in the browser |
353
- | `--port:PORT` | Start the server on a custom port |
354
- | `--database`, `-d` | List all imported databases |
355
- | `--database:name` | Select a database by name or id |
356
- | `--database:name --path` | Get the file path of a database |
357
- | `--database:name --size` | Get the size of a database |
358
- | `--database:name --lastopened` | Get the last opened timestamp |
359
- | `--database:name --tables` | Get all table names from a database |
360
- | `--database:name --queries` | List saved queries for a database |
361
- | `--database:name --query:"sql"` | Execute raw SQL and record it in Query History |
362
- | `--database:name --query:"sql" --store:"name"` | Save a raw query in Query History with a name |
363
- | `--database:name --execute:"query"` | Execute a saved query by name |
364
- | `--database:name --saved-query:"query"` | Print a saved query by name |
365
- | `--database:name --notes:"query"` | Print saved notes for a query |
366
- | `--database:name --export:"query" --format:csv\|tsv\|md\|json` | Set query export format |
367
- | `--database:name --documents` | List Markdown documents for a database |
368
- | `--database:name --documents:"document"` | Print a document's Markdown content |
369
- | `--database:name --documents:"document" --export` | Export a document as Markdown |
370
- | `--database:name --table:"table"` | Print table metadata |
371
- | `--database:name --table:"table" --export:"pk"` | Export one row as JSON |
372
-
373
- Legacy aliases such as `--database-path:name`, `--database-size:name`, `--database-lastopened:name`, `--database-tables:name`, and `--database:name --sqleditor:"query"` still work.
374
-
375
- ### SQL editor CLI example
376
-
377
- Saved queries created in the graphical SQL Editor can also be executed through the CLI. To execute one, run:
378
-
379
- ```bash
380
- sqlite-hub --database:Unit-00 --execute:"Group by creation Year"
381
- ```
382
-
383
- Example output:
384
-
385
- ```bash
386
- Executing: Group by creation Year
387
- SQL: SELECT STRFTIME('%Y', creation_time, 'unixepoch') AS creation_year, COUNT(*) AS channel_count FROM channels WHERE creation_time IS NOT NU...
388
- ────────────────────────────────────────────────────────────
389
-
390
- Statement count: 1
391
- Timing: 1ms
392
-
393
- Statement 1 (resultSet):
394
- Rows: 3
395
- Columns: creation_year, channel_count
396
-
397
- Results:
398
- [0] 2024 | 11
399
- [1] 2025 | 47
400
- [2] 2026 | 40
401
- ```
219
+ SQLite Hub ships with a built-in CLI for starting the app, inspecting imported
220
+ databases, executing raw or saved SQL, exporting query results, exporting single
221
+ rows as JSON, and working with Markdown documents. See the
222
+ [CLI documentation](./docs/CLI.md) for commands, flags, and examples.
402
223
 
403
224
  ## API
404
225
 
package/bin/sqlite-hub.js CHANGED
@@ -566,7 +566,9 @@ function printExecutionResult(result) {
566
566
  }
567
567
 
568
568
  function executeSavedQuery({ databaseService, conn, queryName }) {
569
- const { query: matchingQuery, result } = databaseService.executeSavedQuery(conn.id, queryName);
569
+ const { query: matchingQuery, result } = databaseService.executeSavedQuery(conn.id, queryName, {
570
+ executedBy: "cli",
571
+ });
570
572
 
571
573
  console.log(`\nExecuting: ${getQueryTitle(matchingQuery)}`);
572
574
  console.log(`SQL: ${matchingQuery.previewSql || matchingQuery.rawSql}`);
@@ -576,7 +578,10 @@ function executeSavedQuery({ databaseService, conn, queryName }) {
576
578
  }
577
579
 
578
580
  function executeRawQuery({ databaseService, conn, sql, storeName = null }) {
579
- const { result, storedQuery } = databaseService.executeRawQuery(conn.id, sql, { storeName });
581
+ const { result, storedQuery } = databaseService.executeRawQuery(conn.id, sql, {
582
+ storeName,
583
+ executedBy: "cli",
584
+ });
580
585
 
581
586
  console.log(`\nExecuting raw SQL against: ${conn.label}`);
582
587
  console.log('─'.repeat(60));
@@ -105,6 +105,13 @@ export function importSql(payload) {
105
105
  });
106
106
  }
107
107
 
108
+ export function previewImportSql(payload) {
109
+ return request("/api/connections/import-sql/preview", {
110
+ method: "POST",
111
+ body: payload,
112
+ });
113
+ }
114
+
108
115
  export function selectActiveConnection(id) {
109
116
  return request("/api/connections/select-active", {
110
117
  method: "POST",
@@ -131,6 +138,59 @@ export function createActiveConnectionBackup() {
131
138
  });
132
139
  }
133
140
 
141
+ export function getBackups(options = {}) {
142
+ const params = new URLSearchParams();
143
+
144
+ if (options.all) {
145
+ params.set("all", "true");
146
+ }
147
+
148
+ const query = params.toString();
149
+ return request(`/api/backups${query ? `?${query}` : ""}`);
150
+ }
151
+
152
+ export function getBackup(backupId) {
153
+ return request(`/api/backups/${encodeURIComponent(backupId)}`);
154
+ }
155
+
156
+ export function createBackup(payload = {}) {
157
+ return request("/api/backups", {
158
+ method: "POST",
159
+ body: payload,
160
+ });
161
+ }
162
+
163
+ export function updateBackup(backupId, payload = {}) {
164
+ return request(`/api/backups/${encodeURIComponent(backupId)}`, {
165
+ method: "PATCH",
166
+ body: payload,
167
+ });
168
+ }
169
+
170
+ export function restoreBackup(backupId) {
171
+ return request(`/api/backups/${encodeURIComponent(backupId)}/restore`, {
172
+ method: "POST",
173
+ });
174
+ }
175
+
176
+ export function verifyBackup(backupId) {
177
+ return request(`/api/backups/${encodeURIComponent(backupId)}/verify`, {
178
+ method: "POST",
179
+ });
180
+ }
181
+
182
+ export function deleteBackup(backupId) {
183
+ return request(`/api/backups/${encodeURIComponent(backupId)}`, {
184
+ method: "DELETE",
185
+ });
186
+ }
187
+
188
+ export function downloadBackup(backupId) {
189
+ return download(`/api/backups/${encodeURIComponent(backupId)}/download`, {
190
+ fallbackFilename: "backup.sqlite",
191
+ });
192
+ }
193
+
134
194
  export function getOverview() {
135
195
  return request("/api/db/overview");
136
196
  }
@@ -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,
@@ -53,6 +54,10 @@ import {
53
54
  openDeleteEditorRowModal,
54
55
  openDeleteQueryHistoryModal,
55
56
  openDeleteSettingsApiTokenModal,
57
+ openCreateBackupModal,
58
+ openEditBackupNotesModal,
59
+ openDeleteBackupModal,
60
+ openRestoreBackupModal,
56
61
  openDeleteQueryChartModal,
57
62
  openDataExportModal,
58
63
  openDeleteDocumentModal,
@@ -68,6 +73,7 @@ import {
68
73
  openDataRowUpdatePreview,
69
74
  openEditorRowUpdatePreview,
70
75
  refreshCurrentRoute,
76
+ refreshBackups,
71
77
  refreshMediaTaggingPreview,
72
78
  removeConnection,
73
79
  removeCurrentMediaTag,
@@ -83,8 +89,10 @@ import {
83
89
  selectConnection,
84
90
  selectQueryHistoryItem,
85
91
  selectStructureEntry,
92
+ setDocumentsSearchQuery,
86
93
  setTableDesignerSearchQuery,
87
94
  setTableDesignerSqlPreviewVisibility,
95
+ toggleTableDesignerTablesPanel,
88
96
  setDataTableSearchQuery,
89
97
  setStructureTableSearchQuery,
90
98
  toggleStructureTablesPanel,
@@ -103,6 +111,7 @@ import {
103
111
  setEditorPanelVisibility,
104
112
  setEditorTab,
105
113
  submitDeleteChartConfirmation,
114
+ submitDeleteBackupConfirmation,
106
115
  submitDeleteDocumentConfirmation,
107
116
  submitDocumentInsertNote,
108
117
  submitDocumentInsertTable,
@@ -110,6 +119,9 @@ import {
110
119
  submitCreateMediaTaggingMappingTable,
111
120
  submitDeleteQueryHistoryConfirmation,
112
121
  submitDeleteSettingsApiTokenConfirmation,
122
+ submitBackupSafetyChoice,
123
+ submitCreateBackupConfirmation,
124
+ submitEditBackupNotesConfirmation,
113
125
  submitRowUpdatePreviewConfirmation,
114
126
  setQueryHistoryPanelVisibility,
115
127
  sortDataTableByColumn,
@@ -127,6 +139,7 @@ import {
127
139
  toggleChartsResultsPanel,
128
140
  toggleChartsSqlPanel,
129
141
  toggleCurrentDocumentTodo,
142
+ toggleDocumentsPanel,
130
143
  toggleDocumentsPane,
131
144
  setMediaTaggingWorkflowMediaDetailsVisible,
132
145
  setMediaTaggingWorkflowMediaRotationDegrees,
@@ -157,6 +170,7 @@ import {
157
170
  removeCurrentTableDesignerColumn,
158
171
  } from './store.js';
159
172
  import { renderChartsDetail, renderChartsView } from './views/charts.js';
173
+ import { renderBackupsView } from './views/backups.js';
160
174
  import { renderConnectionsView } from './views/connections.js';
161
175
  import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
162
176
  import { renderDocumentsView } from './views/documents.js';
@@ -239,6 +253,7 @@ const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
239
253
  const APP_TITLE = 'SQLite Hub';
240
254
  const ROUTE_TITLE_SEGMENTS = {
241
255
  connections: 'Connections',
256
+ backups: 'Backups',
242
257
  overview: 'Overview',
243
258
  data: 'Data',
244
259
  structure: 'Structure',
@@ -376,21 +391,19 @@ function syncMediaTaggingCurrentMediaUi(button, detailsVisible) {
376
391
  }
377
392
 
378
393
  const nextVisible = Boolean(detailsVisible);
379
- const expandedLabel = button.dataset.expandedLabel || 'Shrink Media Viewer';
380
- const collapsedLabel = button.dataset.collapsedLabel || 'Show Media Viewer';
394
+ const expandedLabel = button.dataset.expandedLabel || 'Hide Viewer';
395
+ const collapsedLabel = button.dataset.collapsedLabel || 'Show Viewer';
381
396
  preview.classList.toggle('media-tagging-preview--meta-hidden', !nextVisible);
397
+ button.classList.toggle('is-active', !nextVisible);
382
398
  button.dataset.nextValue = nextVisible ? 'false' : 'true';
383
399
  button.setAttribute('aria-expanded', nextVisible ? 'true' : 'false');
400
+ button.setAttribute('aria-pressed', nextVisible ? 'false' : 'true');
384
401
 
385
- if (nextVisible) {
386
- const icon = document.createElement('span');
402
+ const icon = document.createElement('span');
387
403
 
388
- icon.className = 'material-symbols-outlined';
389
- icon.textContent = 'visibility_off';
390
- button.replaceChildren(icon, document.createTextNode(` ${expandedLabel}`));
391
- } else {
392
- button.textContent = collapsedLabel;
393
- }
404
+ icon.className = 'material-symbols-outlined';
405
+ icon.textContent = nextVisible ? 'visibility_off' : 'visibility';
406
+ button.replaceChildren(icon, document.createTextNode(` ${nextVisible ? expandedLabel : collapsedLabel}`));
394
407
 
395
408
  return true;
396
409
  }
@@ -1053,6 +1066,8 @@ function resolveView(state) {
1053
1066
  return renderLandingView(state);
1054
1067
  case 'connections':
1055
1068
  return renderConnectionsView(state);
1069
+ case 'backups':
1070
+ return renderBackupsView(state);
1056
1071
  case 'overview':
1057
1072
  return renderOverviewView(state);
1058
1073
  case 'charts':
@@ -1585,6 +1600,27 @@ function formatCurrentQuery() {
1585
1600
  showToast('SQL query formatted.', 'success');
1586
1601
  }
1587
1602
 
1603
+ async function copyCurrentQueryToClipboard() {
1604
+ const currentQuery = getState().editor.sqlText ?? '';
1605
+
1606
+ if (!currentQuery.trim()) {
1607
+ showToast('No SQL query to copy.', 'alert');
1608
+ return;
1609
+ }
1610
+
1611
+ if (!navigator.clipboard?.writeText) {
1612
+ showToast('Clipboard API is not available.', 'alert');
1613
+ return;
1614
+ }
1615
+
1616
+ try {
1617
+ await navigator.clipboard.writeText(currentQuery);
1618
+ showToast('SQL query copied.', 'success');
1619
+ } catch (error) {
1620
+ showToast('SQL query could not be copied.', 'alert');
1621
+ }
1622
+ }
1623
+
1588
1624
  const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
1589
1625
 
1590
1626
  function getOpenableUrl(value) {
@@ -1795,6 +1831,14 @@ function closeCopyColumnMenus(exceptMenu = null) {
1795
1831
  });
1796
1832
  }
1797
1833
 
1834
+ function closeDropdownButtons(exceptDropdown = null) {
1835
+ document.querySelectorAll('[data-dropdown-button][open]').forEach(dropdown => {
1836
+ if (dropdown !== exceptDropdown && dropdown instanceof HTMLDetailsElement) {
1837
+ dropdown.open = false;
1838
+ }
1839
+ });
1840
+ }
1841
+
1798
1842
  function closeSidebarDatabasePickers(exceptPicker = null) {
1799
1843
  document.querySelectorAll('.sidebar-db-picker[open]').forEach(picker => {
1800
1844
  if (picker !== exceptPicker && picker instanceof HTMLDetailsElement) {
@@ -2446,6 +2490,33 @@ async function handleAction(actionNode) {
2446
2490
  case 'create-backup':
2447
2491
  await createActiveConnectionBackup();
2448
2492
  return;
2493
+ case 'open-create-backup-modal':
2494
+ openCreateBackupModal();
2495
+ return;
2496
+ case 'refresh-backups':
2497
+ await refreshBackups();
2498
+ return;
2499
+ case 'open-restore-backup-modal':
2500
+ openRestoreBackupModal(actionNode.dataset.backupId);
2501
+ return;
2502
+ case 'open-edit-backup-notes-modal':
2503
+ openEditBackupNotesModal(actionNode.dataset.backupId);
2504
+ return;
2505
+ case 'open-delete-backup-modal':
2506
+ openDeleteBackupModal(actionNode.dataset.backupId);
2507
+ return;
2508
+ case 'download-backup':
2509
+ await downloadBackup(actionNode.dataset.backupId);
2510
+ return;
2511
+ case 'backup-safety-create':
2512
+ await submitBackupSafetyChoice('create');
2513
+ return;
2514
+ case 'backup-safety-continue':
2515
+ await submitBackupSafetyChoice('continue');
2516
+ return;
2517
+ case 'backup-safety-cancel':
2518
+ await submitBackupSafetyChoice('cancel');
2519
+ return;
2449
2520
  case 'copy-media-tags': {
2450
2521
  const tags = getState().mediaTagging.tags ?? [];
2451
2522
  const formattedTags = tags
@@ -2476,6 +2547,9 @@ async function handleAction(actionNode) {
2476
2547
  case 'format-current-query':
2477
2548
  formatCurrentQuery();
2478
2549
  return;
2550
+ case 'copy-current-query':
2551
+ await copyCurrentQueryToClipboard();
2552
+ return;
2479
2553
  case 'delete-data-row':
2480
2554
  openDeleteDataRowModal(actionNode.dataset.rowIndex);
2481
2555
  return;
@@ -2656,6 +2730,12 @@ async function handleAction(actionNode) {
2656
2730
  case 'toggle-structure-tables':
2657
2731
  toggleStructureTablesPanel();
2658
2732
  return;
2733
+ case 'toggle-table-designer-tables':
2734
+ toggleTableDesignerTablesPanel();
2735
+ return;
2736
+ case 'toggle-documents-panel':
2737
+ toggleDocumentsPanel();
2738
+ return;
2659
2739
  case 'select-structure-entry':
2660
2740
  if (actionNode.dataset.entryName) {
2661
2741
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -2864,6 +2944,20 @@ document.addEventListener('click', event => {
2864
2944
  });
2865
2945
  }
2866
2946
 
2947
+ const dropdownButton = target.closest('[data-dropdown-button]');
2948
+
2949
+ if (!dropdownButton) {
2950
+ closeDropdownButtons();
2951
+ } else if (target.closest('.dropdown-button__toggle')) {
2952
+ window.requestAnimationFrame(() => {
2953
+ if (dropdownButton instanceof HTMLDetailsElement && dropdownButton.open) {
2954
+ closeDropdownButtons(dropdownButton);
2955
+ }
2956
+ });
2957
+ } else if (target.closest('.dropdown-button__item')) {
2958
+ closeDropdownButtons();
2959
+ }
2960
+
2867
2961
  const sidebarDatabasePicker = target.closest('.sidebar-db-picker');
2868
2962
 
2869
2963
  if (!sidebarDatabasePicker) {
@@ -2983,12 +3077,30 @@ document.addEventListener('keydown', event => {
2983
3077
  return;
2984
3078
  }
2985
3079
 
3080
+ if (document.querySelector('[data-dropdown-button][open]')) {
3081
+ event.preventDefault();
3082
+ closeDropdownButtons();
3083
+ return;
3084
+ }
3085
+
2986
3086
  if (document.querySelector('.sidebar-db-picker[open]')) {
2987
3087
  event.preventDefault();
2988
3088
  closeSidebarDatabasePickers();
2989
3089
  return;
2990
3090
  }
2991
3091
 
3092
+ if (state.route.name === 'charts' && state.charts.detailPanelVisible) {
3093
+ event.preventDefault();
3094
+ setChartsDetailPanelVisibility(false);
3095
+ return;
3096
+ }
3097
+
3098
+ if (state.editor.historySelectedId !== null || state.editor.historyDetail) {
3099
+ event.preventDefault();
3100
+ clearQueryHistorySelection();
3101
+ return;
3102
+ }
3103
+
2992
3104
  if (
2993
3105
  state.route.name === 'data' &&
2994
3106
  (typeof state.dataBrowser.selectedRowIndex === 'number' || Boolean(state.dataBrowser.selectedRow))
@@ -3051,6 +3163,11 @@ document.addEventListener('input', event => {
3051
3163
  return;
3052
3164
  }
3053
3165
 
3166
+ if (bindNode.dataset.bind === 'documents-search') {
3167
+ setDocumentsSearchQuery(bindNode.value);
3168
+ return;
3169
+ }
3170
+
3054
3171
  if (bindNode.dataset.bind === 'data-search-query') {
3055
3172
  void setDataSearchQuery(bindNode.value);
3056
3173
  return;
@@ -3413,6 +3530,19 @@ document.addEventListener('submit', async event => {
3413
3530
  case 'delete-row-confirm':
3414
3531
  await submitDeleteRowConfirmation();
3415
3532
  return;
3533
+ case 'create-backup':
3534
+ await submitCreateBackupConfirmation({
3535
+ name: String(formData.get('name') ?? ''),
3536
+ notes: String(formData.get('notes') ?? ''),
3537
+ type: String(formData.get('type') ?? 'manual'),
3538
+ });
3539
+ return;
3540
+ case 'edit-backup-notes':
3541
+ await submitEditBackupNotesConfirmation(String(formData.get('notes') ?? ''));
3542
+ return;
3543
+ case 'delete-backup-confirm':
3544
+ await submitDeleteBackupConfirmation();
3545
+ return;
3416
3546
  case 'save-query-chart':
3417
3547
  await saveCurrentQueryChartDraft();
3418
3548
  return;