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.
Files changed (60) 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 +201 -5
  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 +24 -15
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/queryResults.js +1 -1
  11. package/frontend/js/components/rowEditorPanel.js +53 -13
  12. package/frontend/js/components/sidebar.js +1 -0
  13. package/frontend/js/components/topNav.js +1 -4
  14. package/frontend/js/components/workspaceOpenDropdown.js +52 -0
  15. package/frontend/js/router.js +2 -0
  16. package/frontend/js/store.js +450 -38
  17. package/frontend/js/utils/emailPreview.js +28 -0
  18. package/frontend/js/utils/markdownDocuments.js +17 -1
  19. package/frontend/js/utils/riskySql.js +165 -0
  20. package/frontend/js/views/backups.js +204 -0
  21. package/frontend/js/views/charts.js +1 -1
  22. package/frontend/js/views/data.js +42 -16
  23. package/frontend/js/views/documents.js +184 -154
  24. package/frontend/js/views/settings.js +1 -0
  25. package/frontend/js/views/structure.js +47 -33
  26. package/frontend/js/views/tableDesigner.js +23 -0
  27. package/frontend/styles/components.css +133 -0
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +1 -0
  30. package/frontend/styles/views.css +33 -21
  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/importService.js +25 -0
  40. package/server/services/sqlite/sqlExecutor.js +2 -0
  41. package/server/services/storage/appStateStore.js +379 -88
  42. package/tests/api-token-auth.test.js +45 -2
  43. package/tests/backup-manager.test.js +140 -0
  44. package/tests/backups-view.test.js +70 -0
  45. package/tests/charts-height-preset-storage.test.js +60 -0
  46. package/tests/charts-route-state.test.js +144 -0
  47. package/tests/cli-service-delegation.test.js +97 -0
  48. package/tests/connection-removal.test.js +52 -0
  49. package/tests/database-command-service.test.js +37 -2
  50. package/tests/documents-view.test.js +132 -0
  51. package/tests/dropdown-button.test.js +101 -0
  52. package/tests/email-preview.test.js +89 -0
  53. package/tests/markdown-documents.test.js +24 -0
  54. package/tests/query-editor.test.js +28 -0
  55. package/tests/query-history-detail.test.js +37 -0
  56. package/tests/query-history-header.test.js +30 -0
  57. package/tests/risky-sql.test.js +30 -0
  58. package/tests/row-editor-null-values.test.js +53 -0
  59. package/tests/settings-view.test.js +1 -0
  60. package/tests/structure-view.test.js +60 -0
@@ -1,3 +1,4 @@
1
+ import { renderDropdownButton } from '../components/dropdownButton.js';
1
2
  import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
2
3
  import { renderMarkdownPreview } from '../utils/markdownDocuments.js';
3
4
 
@@ -100,164 +101,191 @@ function renderDocumentsSidebar(documents) {
100
101
  `;
101
102
  }
102
103
 
103
- function renderDocumentToolbar(documents) {
104
+ function renderDocumentImportFileInput() {
105
+ return `
106
+ <input
107
+ accept=".md,.markdown,text/markdown,text/plain"
108
+ data-bind="document-import-file"
109
+ hidden
110
+ type="file"
111
+ />
112
+ `;
113
+ }
114
+
115
+ function renderNewDocumentDropdown(documents, className = '') {
116
+ return `
117
+ <div class="${escapeHtml(className)}">
118
+ ${renderDropdownButton({
119
+ disabled: documents.saving,
120
+ icon: 'add',
121
+ label: 'New Document',
122
+ title: 'New document',
123
+ items: [
124
+ {
125
+ action: 'create-document',
126
+ icon: 'draft',
127
+ label: 'Blank Page',
128
+ },
129
+ {
130
+ action: 'import-document-markdown',
131
+ icon: 'upload_file',
132
+ label: 'Import .md',
133
+ },
134
+ ],
135
+ })}
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ function renderDocumentsPanelToggle(documents) {
141
+ const visible = documents.documentsVisible !== false;
142
+
143
+ return `
144
+ <button
145
+ class="standard-button panel-toggle-button ${visible ? '' : 'is-active'}"
146
+ aria-pressed="${visible ? 'false' : 'true'}"
147
+ data-action="toggle-documents-panel"
148
+ type="button"
149
+ >
150
+ <span class="material-symbols-outlined">${visible ? 'visibility_off' : 'visibility'}</span>
151
+ ${visible ? 'Hide Documents' : 'Show Documents'}
152
+ </button>
153
+ `;
154
+ }
155
+
156
+ function renderDocumentPaneToggle(documents, pane) {
157
+ const isEditor = pane === 'editor';
158
+ const visible = isEditor ? documents.editorVisible : documents.previewVisible;
159
+ const label = `${visible ? 'Hide' : 'Show'} ${isEditor ? 'Editor' : 'Preview'}`;
160
+ const icon = visible ? 'visibility_off' : isEditor ? 'edit_note' : 'visibility';
161
+
162
+ return `
163
+ <button
164
+ class="standard-button panel-toggle-button documents-pane__toggle ${visible ? '' : 'is-active'}"
165
+ aria-pressed="${visible ? 'false' : 'true'}"
166
+ data-action="toggle-document-pane"
167
+ data-pane="${escapeHtml(pane)}"
168
+ type="button"
169
+ >
170
+ <span class="material-symbols-outlined">${icon}</span>
171
+ ${label}
172
+ </button>
173
+ `;
174
+ }
175
+
176
+ function renderDocumentsTitlebar(documents, options = {}) {
177
+ const { showDocumentActions = false, showFilename = true } = options;
104
178
  const disabled = documents.saving || documents.detailLoading || !documents.selectedId;
105
179
 
106
180
  return `
107
181
  <div class="documents-titlebar">
108
- <label class="documents-filename-field">
109
- <input
110
- class="control-input"
111
- data-bind="document-field"
112
- data-field="filename"
113
- name="filename"
114
- spellcheck="false"
115
- type="text"
116
- value="${escapeHtml(documents.draftFilename)}"
117
- ${!documents.selectedId ? 'disabled' : ''}
118
- />
119
- </label>
120
- <button
121
- class="signature-button documents-create-button"
122
- data-action="create-document"
123
- type="button"
124
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
125
- >
126
- <span class="material-symbols-outlined">add</span>
127
- New Document
128
- </button>
129
- </div>
130
- <div class="documents-toolbar">
131
-
132
-
133
-
134
- <div class="documents-toolbar__actions">
135
- <button
136
- class="standard-button panel-toggle-button ${documents.editorVisible ? '' : 'is-active'}"
137
- aria-pressed="${documents.editorVisible ? 'false' : 'true'}"
138
- data-action="toggle-document-pane"
139
- data-pane="editor"
140
- type="button"
141
- >
142
- <span class="material-symbols-outlined">${documents.editorVisible ? 'visibility_off' : 'edit_note'}</span>
143
- ${documents.editorVisible ? 'Hide Editor' : 'Show Editor'}
144
- </button>
145
- <button
146
- class="standard-button panel-toggle-button ${documents.previewVisible ? '' : 'is-active'}"
147
- aria-pressed="${documents.previewVisible ? 'false' : 'true'}"
148
- data-action="toggle-document-pane"
149
- data-pane="preview"
150
- type="button"
151
- >
152
- <span class="material-symbols-outlined">${documents.previewVisible ? 'visibility_off' : 'visibility'}</span>
153
- ${documents.previewVisible ? 'Hide Preview' : 'Show Preview'}
154
- </button>
155
- <input
156
- accept=".md,.markdown,text/markdown,text/plain"
157
- data-bind="document-import-file"
158
- hidden
159
- type="file"
160
- />
161
- </div>
162
- <div class="documents-toolbar__actions">
163
- <button
164
- class="standard-button"
165
- data-action="open-document-insert-table-modal"
166
- type="button"
167
- ${disabled ? 'disabled aria-disabled="true"' : ''}
168
- >
169
- <span class="material-symbols-outlined">table_chart</span>
170
- Insert Table
171
- </button>
172
- <button
173
- class="standard-button"
174
- data-action="open-document-insert-note-modal"
175
- type="button"
176
- ${disabled ? 'disabled aria-disabled="true"' : ''}
177
- >
178
- <span class="material-symbols-outlined">note_add</span>
179
- Insert Note
180
- </button>
181
- <button
182
- class="standard-button"
183
- data-action="export-document-markdown"
184
- type="button"
185
- ${disabled ? 'disabled aria-disabled="true"' : ''}
186
- >
187
- <span class="material-symbols-outlined">download</span>
188
- Export .md
189
- </button>
190
- <button
191
- class="standard-button"
192
- data-action="import-document-markdown"
193
- type="button"
194
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
195
- >
196
- <span class="material-symbols-outlined">upload_file</span>
197
- Import .md
198
- </button>
199
- </div>
200
- <div class="documents-toolbar__actions">
201
- <button
202
- class="standard-button"
203
- data-action="delete-document"
204
- type="button"
205
- ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
206
- >
207
- <span class="material-symbols-outlined">delete</span>
208
- Delete
209
- </button>
210
- <button
211
- class="signature-button"
212
- data-action="save-document"
213
- type="button"
214
- ${disabled || !documents.dirty ? 'disabled aria-disabled="true"' : ''}
215
- >
216
- <span class="material-symbols-outlined">save</span>
217
- ${documents.saving ? 'Saving...' : 'Save'}
218
- </button>
219
- </div>
182
+ ${renderDocumentsPanelToggle(documents)}
183
+ ${
184
+ showFilename
185
+ ? `
186
+ <label class="documents-filename-field">
187
+ <input
188
+ class="control-input"
189
+ data-bind="document-field"
190
+ data-field="filename"
191
+ name="filename"
192
+ spellcheck="false"
193
+ type="text"
194
+ value="${escapeHtml(documents.draftFilename)}"
195
+ ${!documents.selectedId ? 'disabled' : ''}
196
+ />
197
+ </label>
198
+ `
199
+ : ''
200
+ }
201
+ ${renderNewDocumentDropdown(documents, 'documents-create-button')}
202
+ ${
203
+ showDocumentActions
204
+ ? `
205
+ ${renderDropdownButton({
206
+ disabled,
207
+ icon: 'add_box',
208
+ label: 'Insert',
209
+ title: 'Insert content',
210
+ items: [
211
+ {
212
+ action: 'open-document-insert-table-modal',
213
+ icon: 'table_chart',
214
+ label: 'Insert Table',
215
+ },
216
+ {
217
+ action: 'open-document-insert-note-modal',
218
+ icon: 'note_add',
219
+ label: 'Insert Note',
220
+ },
221
+ ],
222
+ })}
223
+ <button
224
+ class="standard-button"
225
+ data-action="export-document-markdown"
226
+ type="button"
227
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
228
+ >
229
+ <span class="material-symbols-outlined">download</span>
230
+ Export .md
231
+ </button>
232
+ <button
233
+ class="delete-button"
234
+ data-action="delete-document"
235
+ type="button"
236
+ ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
237
+ >
238
+ <span class="material-symbols-outlined">delete</span>
239
+ Delete
240
+ </button>
241
+ `
242
+ : ''
243
+ }
220
244
  </div>
221
245
  `;
222
246
  }
223
247
 
224
248
  function renderDocumentEditor(documents) {
225
- if (!documents.editorVisible) {
226
- return '';
227
- }
228
-
229
249
  return `
230
- <section class="documents-pane documents-pane--editor">
250
+ <section class="documents-pane documents-pane--editor ${documents.editorVisible ? '' : 'documents-pane--collapsed'}">
231
251
  <div class="documents-pane__header">
232
- <span>Editor</span>
233
- <span>${formatNumber(documents.draftContent.length)} chars</span>
252
+ ${renderDocumentPaneToggle(documents, 'editor')}
253
+ <span class="documents-pane__meta">${formatNumber(documents.draftContent.length)} chars</span>
234
254
  </div>
235
- <textarea
236
- class="documents-editor-input custom-scrollbar"
237
- data-bind="document-field"
238
- data-field="content"
239
- name="content"
240
- spellcheck="true"
241
- ${!documents.selectedId ? 'disabled' : ''}
242
- >${escapeHtml(documents.draftContent)}</textarea>
255
+ ${
256
+ documents.editorVisible
257
+ ? `
258
+ <textarea
259
+ class="documents-editor-input custom-scrollbar"
260
+ data-bind="document-field"
261
+ data-field="content"
262
+ name="content"
263
+ spellcheck="true"
264
+ ${!documents.selectedId ? 'disabled' : ''}
265
+ >${escapeHtml(documents.draftContent)}</textarea>
266
+ `
267
+ : ''
268
+ }
243
269
  </section>
244
270
  `;
245
271
  }
246
272
 
247
273
  function renderDocumentPreview(documents) {
248
- if (!documents.previewVisible) {
249
- return '';
250
- }
251
-
252
274
  return `
253
- <section class="documents-pane documents-pane--preview">
275
+ <section class="documents-pane documents-pane--preview ${documents.previewVisible ? '' : 'documents-pane--collapsed'}">
254
276
  <div class="documents-pane__header">
255
- <span>Preview</span>
256
- <span>${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
257
- </div>
258
- <div class="document-markdown-preview custom-scrollbar" data-document-preview>
259
- ${renderMarkdownPreview(documents.draftContent)}
277
+ ${renderDocumentPaneToggle(documents, 'preview')}
278
+ <span class="documents-pane__meta">${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
260
279
  </div>
280
+ ${
281
+ documents.previewVisible
282
+ ? `
283
+ <div class="document-markdown-preview custom-scrollbar" data-document-preview>
284
+ ${renderMarkdownPreview(documents.draftContent)}
285
+ </div>
286
+ `
287
+ : ''
288
+ }
261
289
  </section>
262
290
  `;
263
291
  }
@@ -269,15 +297,7 @@ function renderEmptyDocumentsState(documents) {
269
297
  <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
270
298
  No Documents
271
299
  </p>
272
- <button
273
- class="signature-button mt-4"
274
- data-action="create-document"
275
- type="button"
276
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
277
- >
278
- <span class="material-symbols-outlined">add</span>
279
- New Document
280
- </button>
300
+ ${renderNewDocumentDropdown(documents, 'mt-4')}
281
301
  </div>
282
302
  `;
283
303
  }
@@ -286,6 +306,7 @@ function renderDocumentDetail(documents) {
286
306
  if (documents.loading && !documents.items.length) {
287
307
  return `
288
308
  <main class="documents-view__detail">
309
+ ${renderDocumentsTitlebar(documents, { showFilename: false })}
289
310
  <div class="documents-empty-state">
290
311
  <span class="material-symbols-outlined">sync</span>
291
312
  <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
@@ -299,18 +320,26 @@ function renderDocumentDetail(documents) {
299
320
  if (!documents.selectedId) {
300
321
  return `
301
322
  <main class="documents-view__detail">
323
+ ${renderDocumentsTitlebar(documents, { showFilename: false })}
302
324
  ${renderEmptyDocumentsState(documents)}
303
325
  </main>
304
326
  `;
305
327
  }
306
328
 
307
- const paneCount = Number(documents.editorVisible) + Number(documents.previewVisible);
329
+ const workspaceClasses = [
330
+ 'documents-workspace',
331
+ documents.editorVisible && documents.previewVisible ? 'documents-workspace--split' : '',
332
+ !documents.editorVisible ? 'documents-workspace--editor-collapsed' : '',
333
+ !documents.previewVisible ? 'documents-workspace--preview-collapsed' : '',
334
+ ]
335
+ .filter(Boolean)
336
+ .join(' ');
308
337
 
309
338
  return `
310
339
  <main class="documents-view__detail">
311
- ${renderDocumentToolbar(documents)}
340
+ ${renderDocumentsTitlebar(documents, { showDocumentActions: true })}
312
341
  ${documents.saveError ? `<div class="documents-error">${escapeHtml(documents.saveError.message)}</div>` : ''}
313
- <div class="documents-workspace ${paneCount > 1 ? 'documents-workspace--split' : ''}">
342
+ <div class="${workspaceClasses}">
314
343
  ${renderDocumentEditor(documents)}
315
344
  ${renderDocumentPreview(documents)}
316
345
  </div>
@@ -327,13 +356,14 @@ export function renderDocumentsView(state) {
327
356
  }
328
357
 
329
358
  const documents = state.documents;
359
+ const documentsVisible = documents.documentsVisible !== false;
330
360
 
331
361
  return {
332
- main: `<div class="documents-view">
333
- ${renderDocumentsSidebar(documents)}
362
+ main: `<div class="documents-view ${documentsVisible ? 'documents-view--with-subnavi' : ''}">
363
+ ${renderDocumentImportFileInput()}
364
+ ${documentsVisible ? renderDocumentsSidebar(documents) : ''}
334
365
  ${renderDocumentDetail(documents)}
335
- </div>
336
- </div>`,
366
+ </div>`,
337
367
  panel: '',
338
368
  };
339
369
  }
@@ -139,6 +139,7 @@ function renderSettingsContent(state) {
139
139
  type="button"
140
140
  ${tokenSaving ? 'disabled' : ''}
141
141
  >
142
+ <span class="material-symbols-outlined text-sm">delete</span>
142
143
  Delete
143
144
  </button>
144
145
  </div>
@@ -1,4 +1,6 @@
1
1
  import { clearInspector, renderDdlSection, renderInspector } from '../components/structureGraph.js';
2
+ import { renderDropdownButton } from '../components/dropdownButton.js';
3
+ import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
2
4
  import { escapeHtml, formatNumber } from '../utils/format.js';
3
5
 
4
6
  function renderEntryGroup(title, entries, activeName, options = {}) {
@@ -230,39 +232,51 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
230
232
  </button>
231
233
  </div>
232
234
  <div class="structure-graph__toolbar-actions">
233
- <button
234
- class="${toolbarButtonClass}"
235
- data-structure-graph-action="fit"
236
- type="button"
237
- >
238
- <span class="material-symbols-outlined text-sm">fit_screen</span>
239
- Fit Graph
240
- </button>
241
- <button
242
- class="${toolbarButtonClass}"
243
- data-structure-graph-action="relayout"
244
- type="button"
245
- >
246
- <span class="material-symbols-outlined text-sm">device_hub</span>
247
- Recalculate Layout
248
- </button>
249
- <button
250
- class="${toolbarButtonClass}"
251
- data-structure-graph-action="clear"
252
- type="button"
253
- >
254
- <span class="material-symbols-outlined text-sm">close</span>
255
- Clear Selection
256
- </button>
257
- <button
258
- class="${toolbarButtonClass}"
259
- data-structure-graph-action="open-data"
260
- disabled
261
- type="button"
262
- >
263
- <span class="material-symbols-outlined text-sm">table_rows</span>
264
- Open Data
265
- </button>
235
+ ${renderWorkspaceOpenDropdown({
236
+ tableName: selectedGraphTable?.name ?? '',
237
+ destinations: [
238
+ {
239
+ icon: 'table_rows',
240
+ key: 'data',
241
+ label: 'Data',
242
+ target: tableName => `/data/${encodeURIComponent(tableName)}`,
243
+ },
244
+ {
245
+ icon: 'table_chart',
246
+ key: 'table-designer',
247
+ label: 'Table Designer',
248
+ target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
249
+ },
250
+ {
251
+ key: 'sql-editor',
252
+ },
253
+ ],
254
+ })}
255
+ ${renderDropdownButton({
256
+ icon: 'format_shapes',
257
+ label: 'Format',
258
+ title: 'Format graph',
259
+ items: [
260
+ {
261
+ action: 'fit',
262
+ actionAttribute: 'data-structure-graph-action',
263
+ icon: 'fit_screen',
264
+ label: 'Fit Graph',
265
+ },
266
+ {
267
+ action: 'relayout',
268
+ actionAttribute: 'data-structure-graph-action',
269
+ icon: 'device_hub',
270
+ label: 'Recalculate Layout',
271
+ },
272
+ {
273
+ action: 'clear',
274
+ actionAttribute: 'data-structure-graph-action',
275
+ icon: 'close',
276
+ label: 'Clear Selection',
277
+ },
278
+ ],
279
+ })}
266
280
  </div>
267
281
  <div class="structure-graph__toolbar-inspector">
268
282
  <button
@@ -1,6 +1,7 @@
1
1
  import { renderTableDesignerEditor } from "../components/tableDesignerEditor.js";
2
2
  import { renderTableDesignerSidebar } from "../components/tableDesignerSidebar.js";
3
3
  import { renderTableDesignerSqlPreview } from "../components/tableDesignerSqlPreview.js";
4
+ import { renderWorkspaceOpenDropdown } from "../components/workspaceOpenDropdown.js";
4
5
  import { escapeHtml } from "../utils/format.js";
5
6
 
6
7
  function renderRouteError(error) {
@@ -18,6 +19,7 @@ function renderRouteError(error) {
18
19
 
19
20
  function renderWorkspaceToolbar(state) {
20
21
  const tablesVisible = state.tableDesigner.tablesVisible !== false;
22
+ const tableName = state.tableDesigner.selectedTableName ?? state.tableDesigner.draft?.tableName ?? "";
21
23
 
22
24
  return `
23
25
  <div class="table-designer-workspace__toolbar workspace-header">
@@ -33,6 +35,27 @@ function renderWorkspaceToolbar(state) {
33
35
  </button>
34
36
  </div>
35
37
  <div class="table-designer-workspace__toolbar-right">
38
+ ${renderWorkspaceOpenDropdown({
39
+ tableName,
40
+ disabled: !state.tableDesigner.selectedTableName,
41
+ destinations: [
42
+ {
43
+ icon: "table_rows",
44
+ key: "data",
45
+ label: "Data",
46
+ target: name => `/data/${encodeURIComponent(name)}`,
47
+ },
48
+ {
49
+ icon: "account_tree",
50
+ key: "structure",
51
+ label: "Structure",
52
+ target: name => `/structure/${encodeURIComponent(name)}`,
53
+ },
54
+ {
55
+ key: "sql-editor",
56
+ },
57
+ ],
58
+ })}
36
59
  <button
37
60
  class="standard-button"
38
61
  data-action="import-table-designer-csv"