sqlite-hub 1.1.1 → 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 (48) 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 +110 -0
  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/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +2 -0
  13. package/frontend/js/store.js +383 -37
  14. package/frontend/js/utils/riskySql.js +165 -0
  15. package/frontend/js/views/backups.js +176 -0
  16. package/frontend/js/views/charts.js +1 -1
  17. package/frontend/js/views/documents.js +184 -154
  18. package/frontend/js/views/structure.js +26 -24
  19. package/frontend/styles/components.css +128 -0
  20. package/frontend/styles/tailwind.generated.css +1 -1
  21. package/frontend/styles/views.css +33 -21
  22. package/package.json +2 -1
  23. package/server/routes/backups.js +120 -0
  24. package/server/routes/connections.js +26 -3
  25. package/server/routes/externalApi.js +6 -2
  26. package/server/server.js +3 -1
  27. package/server/services/databaseCommandService.js +4 -3
  28. package/server/services/sqlite/backupService.js +497 -66
  29. package/server/services/sqlite/connectionManager.js +2 -2
  30. package/server/services/sqlite/importService.js +25 -0
  31. package/server/services/sqlite/sqlExecutor.js +2 -0
  32. package/server/services/storage/appStateStore.js +379 -88
  33. package/tests/api-token-auth.test.js +45 -2
  34. package/tests/backup-manager.test.js +140 -0
  35. package/tests/backups-view.test.js +64 -0
  36. package/tests/charts-height-preset-storage.test.js +60 -0
  37. package/tests/charts-route-state.test.js +144 -0
  38. package/tests/cli-service-delegation.test.js +97 -0
  39. package/tests/connection-removal.test.js +52 -0
  40. package/tests/database-command-service.test.js +37 -2
  41. package/tests/documents-view.test.js +132 -0
  42. package/tests/dropdown-button.test.js +75 -0
  43. package/tests/query-editor.test.js +28 -0
  44. package/tests/query-history-detail.test.js +37 -0
  45. package/tests/query-history-header.test.js +30 -0
  46. package/tests/risky-sql.test.js +30 -0
  47. package/tests/row-editor-null-values.test.js +24 -0
  48. package/tests/structure-view.test.js +56 -0
@@ -1,29 +1,25 @@
1
- import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from '../utils/format.js';
2
2
  import {
3
- buildCopyColumnText,
4
- buildCopyColumnPreviewText,
5
- getCopyColumnActionLabel,
6
- getCopyColumnExportMetadata,
7
- isMarkdownTodoCopyColumnMode,
8
- } from "../utils/copyColumnExport.js";
9
- import { renderConnectionLogo } from "./connectionLogo.js";
10
- import { renderTextInput } from "./formControls.js";
3
+ buildCopyColumnText,
4
+ buildCopyColumnPreviewText,
5
+ getCopyColumnActionLabel,
6
+ getCopyColumnExportMetadata,
7
+ isMarkdownTodoCopyColumnMode,
8
+ } from '../utils/copyColumnExport.js';
9
+ import { renderConnectionLogo } from './connectionLogo.js';
10
+ import { renderTextInput } from './formControls.js';
11
+ import { analyzeQueryChartResult, getQueryChartTypeLabel, QUERY_CHART_TYPES } from '../lib/queryCharts.js';
11
12
  import {
12
- analyzeQueryChartResult,
13
- getQueryChartTypeLabel,
14
- QUERY_CHART_TYPES,
15
- } from "../lib/queryCharts.js";
16
- import {
17
- hasDefaultMediaTaggingTagTable,
18
- hasDefaultMediaTaggingMappingTable,
19
- MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
20
- MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL,
21
- MEDIA_TAGGING_DEFAULT_TAG_TABLE,
22
- MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
23
- } from "../lib/mediaTaggingDefaults.js";
24
-
25
- function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
26
- return `
13
+ hasDefaultMediaTaggingTagTable,
14
+ hasDefaultMediaTaggingMappingTable,
15
+ MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
16
+ MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL,
17
+ MEDIA_TAGGING_DEFAULT_TAG_TABLE,
18
+ MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
19
+ } from '../lib/mediaTaggingDefaults.js';
20
+
21
+ function renderField({ label, name, type = 'text', placeholder = '', value = '' }) {
22
+ return `
27
23
  <label class="block space-y-2">
28
24
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
29
25
  ${escapeHtml(label)}
@@ -34,14 +30,14 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
34
30
  }
35
31
 
36
32
  function renderCheckboxField({ label, name, checked = false, text }) {
37
- return `
33
+ return `
38
34
  <label class="flex flex-col gap-2">
39
35
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
40
36
  ${escapeHtml(label)}
41
37
  </span>
42
38
  <span class="standard-checkbox">
43
39
  <input
44
- ${checked ? "checked" : ""}
40
+ ${checked ? 'checked' : ''}
45
41
  name="${escapeHtml(name)}"
46
42
  type="checkbox"
47
43
  />
@@ -51,15 +47,15 @@ function renderCheckboxField({ label, name, checked = false, text }) {
51
47
  `;
52
48
  }
53
49
 
54
- function renderSqlPreviewField(value, minHeightClass = "sql-highlight-shell--tall") {
55
- return `
50
+ function renderSqlPreviewField(value, minHeightClass = 'sql-highlight-shell--tall') {
51
+ return `
56
52
  <div class="sql-highlight-shell ${minHeightClass}">
57
53
  <div class="query-editor-layer sql-highlight-layer">
58
54
  <div
59
55
  aria-hidden="true"
60
56
  class="query-editor-highlight sql-highlight-content"
61
57
  data-query-editor-highlight
62
- >${value ? highlightSql(value) : ""}</div>
58
+ >${value ? highlightSql(value) : ''}</div>
63
59
  <textarea
64
60
  class="query-editor-input sql-highlight-input custom-scrollbar"
65
61
  data-sql-highlight="true"
@@ -72,13 +68,8 @@ function renderSqlPreviewField(value, minHeightClass = "sql-highlight-shell--tal
72
68
  `;
73
69
  }
74
70
 
75
- function renderFileField({
76
- label,
77
- name,
78
- accept = "",
79
- helpText = "",
80
- }) {
81
- return `
71
+ function renderFileField({ label, name, accept = '', helpText = '' }) {
72
+ return `
82
73
  <label class="block space-y-2">
83
74
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
84
75
  ${escapeHtml(label)}
@@ -89,58 +80,54 @@ function renderFileField({
89
80
  name="${escapeHtml(name)}"
90
81
  type="file"
91
82
  />
92
- ${
93
- helpText
94
- ? `<p class="text-[11px] leading-5 text-on-surface-variant/60">${escapeHtml(helpText)}</p>`
95
- : ""
96
- }
83
+ ${helpText ? `<p class="text-[11px] leading-5 text-on-surface-variant/60">${escapeHtml(helpText)}</p>` : ''}
97
84
  </label>
98
85
  `;
99
86
  }
100
87
 
101
- function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
102
- const attributes = [
103
- bind ? ['data-bind="', escapeHtml(bind), '"'].join("") : "",
104
- name ? ['name="', escapeHtml(name), '"'].join("") : "",
105
- ]
106
- .filter(Boolean)
107
- .join(" ");
108
- const optionMarkup = options
109
- .map((option) =>
110
- [
111
- '<option value="',
112
- escapeHtml(option.value),
113
- '" ',
114
- String(option.value) === String(value) ? "selected" : "",
115
- ">",
116
- escapeHtml(option.label),
117
- "</option>",
118
- ].join("")
119
- )
120
- .join("");
121
-
122
- return [
123
- '<label class="block space-y-2">',
124
- '<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">',
125
- escapeHtml(label),
126
- "</span>",
127
- '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" ',
128
- attributes,
129
- ">",
130
- optionMarkup,
131
- "</select></label>",
132
- ].join("");
88
+ function renderSelectField({ label, name, value = '', options = [], bind = '' }) {
89
+ const attributes = [
90
+ bind ? ['data-bind="', escapeHtml(bind), '"'].join('') : '',
91
+ name ? ['name="', escapeHtml(name), '"'].join('') : '',
92
+ ]
93
+ .filter(Boolean)
94
+ .join(' ');
95
+ const optionMarkup = options
96
+ .map(option =>
97
+ [
98
+ '<option value="',
99
+ escapeHtml(option.value),
100
+ '" ',
101
+ String(option.value) === String(value) ? 'selected' : '',
102
+ '>',
103
+ escapeHtml(option.label),
104
+ '</option>',
105
+ ].join(''),
106
+ )
107
+ .join('');
108
+
109
+ return [
110
+ '<label class="block space-y-2">',
111
+ '<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">',
112
+ escapeHtml(label),
113
+ '</span>',
114
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" ',
115
+ attributes,
116
+ '>',
117
+ optionMarkup,
118
+ '</select></label>',
119
+ ].join('');
133
120
  }
134
121
 
135
122
  function renderError(error) {
136
- if (!error) {
137
- return "";
138
- }
123
+ if (!error) {
124
+ return '';
125
+ }
139
126
 
140
- return `
127
+ return `
141
128
  <div class="border border-error/20 bg-error-container/20 px-4 py-3 text-sm text-error">
142
129
  <div class="font-headline text-xs font-bold uppercase tracking-[0.18em]">${escapeHtml(
143
- error.code || "Request failed"
130
+ error.code || 'Request failed',
144
131
  )}</div>
145
132
  <div class="mt-1 text-on-surface">${escapeHtml(error.message)}</div>
146
133
  </div>
@@ -148,7 +135,7 @@ function renderError(error) {
148
135
  }
149
136
 
150
137
  export function renderOpenConnectionForm(modal) {
151
- return `
138
+ return `
152
139
  <form class="space-y-5" data-form="open-connection">
153
140
  <label class="block space-y-2">
154
141
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -156,10 +143,10 @@ export function renderOpenConnectionForm(modal) {
156
143
  </span>
157
144
  <span class="flex items-stretch gap-2">
158
145
  ${renderTextInput({
159
- className: "min-w-0 flex-1",
160
- dataAttributes: { openDatabasePath: true },
161
- name: "path",
162
- placeholder: "/absolute/path/to/database.sqlite",
146
+ className: 'min-w-0 flex-1',
147
+ dataAttributes: { openDatabasePath: true },
148
+ name: 'path',
149
+ placeholder: '/absolute/path/to/database.sqlite',
163
150
  })}
164
151
  <button
165
152
  class="standard-button flex-none"
@@ -175,14 +162,14 @@ export function renderOpenConnectionForm(modal) {
175
162
  </span>
176
163
  </label>
177
164
  ${renderField({
178
- label: "Label",
179
- name: "label",
180
- placeholder: "Optional display name",
165
+ label: 'Label',
166
+ name: 'label',
167
+ placeholder: 'Optional display name',
181
168
  })}
182
169
  ${renderCheckboxField({
183
- label: "Open read-only",
184
- name: "readOnly",
185
- text: "Open read-only",
170
+ label: 'Open read-only',
171
+ name: 'readOnly',
172
+ text: 'Open read-only',
186
173
  })}
187
174
  ${renderError(modal.error)}
188
175
  <div class="flex items-center justify-between gap-3 pt-2">
@@ -197,7 +184,7 @@ export function renderOpenConnectionForm(modal) {
197
184
  class="standard-button"
198
185
  type="submit"
199
186
  >
200
- ${modal.submitting ? "Opening..." : "Open Database"}
187
+ ${modal.submitting ? 'Opening...' : 'Open Database'}
201
188
  </button>
202
189
  </div>
203
190
  </form>
@@ -205,22 +192,22 @@ export function renderOpenConnectionForm(modal) {
205
192
  }
206
193
 
207
194
  function renderEditConnectionForm(modal) {
208
- const connection = modal.connection ?? {};
195
+ const connection = modal.connection ?? {};
209
196
 
210
- return `
197
+ return `
211
198
  <form class="space-y-5" data-form="edit-connection">
212
- <input name="connectionId" type="hidden" value="${escapeHtml(connection.id ?? "")}" />
199
+ <input name="connectionId" type="hidden" value="${escapeHtml(connection.id ?? '')}" />
213
200
  ${renderField({
214
- label: "SQLite File Path",
215
- name: "path",
216
- placeholder: "/absolute/path/to/database.sqlite",
217
- value: connection.path ?? "",
201
+ label: 'SQLite File Path',
202
+ name: 'path',
203
+ placeholder: '/absolute/path/to/database.sqlite',
204
+ value: connection.path ?? '',
218
205
  })}
219
206
  ${renderField({
220
- label: "Label",
221
- name: "label",
222
- placeholder: "Optional display name",
223
- value: connection.label ?? "",
207
+ label: 'Label',
208
+ name: 'label',
209
+ placeholder: 'Optional display name',
210
+ value: connection.label ?? '',
224
211
  })}
225
212
  <div class="space-y-3">
226
213
  <span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -228,35 +215,35 @@ function renderEditConnectionForm(modal) {
228
215
  </span>
229
216
  <div class="flex flex-wrap items-center gap-4 border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
230
217
  ${renderConnectionLogo(connection, {
231
- containerClass:
232
- "flex h-16 w-16 items-center justify-center overflow-hidden border border-outline-variant/20 bg-surface-container-highest",
233
- imageClassName: "h-full w-full object-cover",
234
- iconClassName: "text-2xl text-primary-container",
218
+ containerClass:
219
+ 'flex h-16 w-16 items-center justify-center overflow-hidden border border-outline-variant/20 bg-surface-container-highest',
220
+ imageClassName: 'h-full w-full object-cover',
221
+ iconClassName: 'text-2xl text-primary-container',
235
222
  })}
236
223
  <div class="min-w-0 flex-1">
237
224
  ${renderFileField({
238
- label: "Upload image",
239
- name: "logoFile",
240
- accept: ".png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp",
241
- helpText: "Allowed formats: PNG, JPG, WEBP. The file is stored in db_logos.",
225
+ label: 'Upload image',
226
+ name: 'logoFile',
227
+ accept: '.png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp',
228
+ helpText: 'Allowed formats: PNG, JPG, WEBP. The file is stored in db_logos.',
242
229
  })}
243
230
  ${
244
- connection.logoUrl
245
- ? renderCheckboxField({
246
- label: "Reset icon",
247
- name: "clearLogo",
248
- text: "Use the default icon again",
249
- })
250
- : ""
231
+ connection.logoUrl
232
+ ? renderCheckboxField({
233
+ label: 'Reset icon',
234
+ name: 'clearLogo',
235
+ text: 'Use the default icon again',
236
+ })
237
+ : ''
251
238
  }
252
239
  </div>
253
240
  </div>
254
241
  </div>
255
242
  ${renderCheckboxField({
256
- label: "Open read-only",
257
- name: "readOnly",
258
- checked: Boolean(connection.readOnly),
259
- text: "Open read-only",
243
+ label: 'Open read-only',
244
+ name: 'readOnly',
245
+ checked: Boolean(connection.readOnly),
246
+ text: 'Open read-only',
260
247
  })}
261
248
  ${renderError(modal.error)}
262
249
  <div class="flex items-center justify-between gap-3 pt-2">
@@ -271,7 +258,7 @@ function renderEditConnectionForm(modal) {
271
258
  class="standard-button"
272
259
  type="submit"
273
260
  >
274
- ${modal.submitting ? "Saving..." : "Save Changes"}
261
+ ${modal.submitting ? 'Saving...' : 'Save Changes'}
275
262
  </button>
276
263
  </div>
277
264
  </form>
@@ -279,7 +266,7 @@ function renderEditConnectionForm(modal) {
279
266
  }
280
267
 
281
268
  export function renderCreateDatabaseForm(modal) {
282
- return `
269
+ return `
283
270
  <form class="space-y-5" data-form="create-connection">
284
271
  <label class="block space-y-2">
285
272
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -287,10 +274,10 @@ export function renderCreateDatabaseForm(modal) {
287
274
  </span>
288
275
  <span class="flex items-stretch gap-2">
289
276
  ${renderTextInput({
290
- className: "min-w-0 flex-1",
291
- dataAttributes: { createDatabasePath: true },
292
- name: "path",
293
- placeholder: "/absolute/path/to/new-database.sqlite",
277
+ className: 'min-w-0 flex-1',
278
+ dataAttributes: { createDatabasePath: true },
279
+ name: 'path',
280
+ placeholder: '/absolute/path/to/new-database.sqlite',
294
281
  })}
295
282
  <button
296
283
  class="standard-button flex-none"
@@ -306,9 +293,9 @@ export function renderCreateDatabaseForm(modal) {
306
293
  </span>
307
294
  </label>
308
295
  ${renderField({
309
- label: "Label",
310
- name: "label",
311
- placeholder: "Optional display name",
296
+ label: 'Label',
297
+ name: 'label',
298
+ placeholder: 'Optional display name',
312
299
  })}
313
300
  ${renderError(modal.error)}
314
301
  <div class="flex items-center justify-between gap-3 pt-2">
@@ -323,7 +310,7 @@ export function renderCreateDatabaseForm(modal) {
323
310
  class="standard-button"
324
311
  type="submit"
325
312
  >
326
- ${modal.submitting ? "Creating..." : "Create Database"}
313
+ ${modal.submitting ? 'Creating...' : 'Create Database'}
327
314
  </button>
328
315
  </div>
329
316
  </form>
@@ -331,65 +318,63 @@ export function renderCreateDatabaseForm(modal) {
331
318
  }
332
319
 
333
320
  function renderImportTargetOptions(state) {
334
- const recentOptions = state.connections.recent
335
- .map((connection) =>
336
- [
337
- '<option value="',
338
- escapeHtml(connection.id),
339
- '">',
340
- escapeHtml(connection.label),
341
- "",
342
- escapeHtml(truncateMiddle(connection.path, 42)),
343
- "</option>",
344
- ].join("")
345
- )
346
- .join("");
347
- const activeOption = state.connections.active
348
- ? '<option value="active">Use active database</option>'
349
- : "";
350
- const recentModeOption = state.connections.recent.length
351
- ? '<option value="recent">Use recent connection</option>'
352
- : "";
353
- const recentConnectionSelect = state.connections.recent.length
354
- ? [
321
+ const recentOptions = state.connections.recent
322
+ .map(connection =>
323
+ [
324
+ '<option value="',
325
+ escapeHtml(connection.id),
326
+ '">',
327
+ escapeHtml(connection.label),
328
+ '',
329
+ escapeHtml(truncateMiddle(connection.path, 42)),
330
+ '</option>',
331
+ ].join(''),
332
+ )
333
+ .join('');
334
+ const activeOption = state.connections.active ? '<option value="active">Use active database</option>' : '';
335
+ const recentModeOption = state.connections.recent.length
336
+ ? '<option value="recent">Use recent connection</option>'
337
+ : '';
338
+ const recentConnectionSelect = state.connections.recent.length
339
+ ? [
340
+ '<label class="block space-y-2">',
341
+ '<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Recent Connection</span>',
342
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetConnectionId">',
343
+ recentOptions,
344
+ '</select></label>',
345
+ ].join('')
346
+ : '';
347
+
348
+ return [
355
349
  '<label class="block space-y-2">',
356
- '<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Recent Connection</span>',
357
- '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetConnectionId">',
358
- recentOptions,
359
- "</select></label>",
360
- ].join("")
361
- : "";
362
-
363
- return [
364
- '<label class="block space-y-2">',
365
- '<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Import Target</span>',
366
- '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetMode">',
367
- activeOption,
368
- recentModeOption,
369
- '<option value="create">Create new database from dump</option>',
370
- '<option value="path">Open explicit target path</option>',
371
- "</select></label>",
372
- recentConnectionSelect,
373
- renderField({
374
- label: "Target Path",
375
- name: "targetPath",
376
- placeholder: "/absolute/path/to/target.sqlite",
377
- }),
378
- renderField({
379
- label: "Target Label",
380
- name: "label",
381
- placeholder: "Optional display name",
382
- }),
383
- ].join("");
350
+ '<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Import Target</span>',
351
+ '<select class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container" name="targetMode">',
352
+ activeOption,
353
+ recentModeOption,
354
+ '<option value="create">Create new database from dump</option>',
355
+ '<option value="path">Open explicit target path</option>',
356
+ '</select></label>',
357
+ recentConnectionSelect,
358
+ renderField({
359
+ label: 'Target Path',
360
+ name: 'targetPath',
361
+ placeholder: '/absolute/path/to/target.sqlite',
362
+ }),
363
+ renderField({
364
+ label: 'Target Label',
365
+ name: 'label',
366
+ placeholder: 'Optional display name',
367
+ }),
368
+ ].join('');
384
369
  }
385
370
 
386
371
  function renderImportSqlForm(modal, state) {
387
- return `
372
+ return `
388
373
  <form class="space-y-5" data-form="import-sql">
389
374
  ${renderField({
390
- label: "SQL Dump Path",
391
- name: "sqlFilePath",
392
- placeholder: "/absolute/path/to/dump.sql",
375
+ label: 'SQL Dump Path',
376
+ name: 'sqlFilePath',
377
+ placeholder: '/absolute/path/to/dump.sql',
393
378
  })}
394
379
  ${renderImportTargetOptions(state)}
395
380
  <p class="text-[11px] leading-6 text-on-surface-variant/60">
@@ -409,124 +394,235 @@ function renderImportSqlForm(modal, state) {
409
394
  class="standard-button"
410
395
  type="submit"
411
396
  >
412
- ${modal.submitting ? "Importing..." : "Import SQL Dump"}
397
+ ${modal.submitting ? 'Importing...' : 'Import SQL Dump'}
413
398
  </button>
414
399
  </div>
415
400
  </form>
416
401
  `;
417
402
  }
418
403
 
419
- function renderDeleteRowConfirmForm(modal) {
420
- const rowPreview = modal.rowPreview ?? [];
421
- const rowLabelMarkup = modal.rowLabel
422
- ? [
404
+ function renderCreateBackupForm(modal) {
405
+ return `
406
+ <form class="space-y-5" data-form="create-backup">
407
+ ${renderField({
408
+ label: 'Name',
409
+ name: 'name',
410
+ placeholder: 'Manual backup',
411
+ value: modal.name ?? '',
412
+ })}
413
+ <label class="block space-y-2">
414
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Notes</span>
415
+ <textarea
416
+ class="control-input min-h-28 w-full resize-y border border-outline-variant/20 bg-surface-container-lowest px-3 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
417
+ name="notes"
418
+ placeholder="Optional backup notes"
419
+ >${escapeHtml(modal.notes ?? '')}</textarea>
420
+ </label>
421
+ <input name="type" type="hidden" value="${escapeHtml(modal.backupType ?? 'manual')}" />
422
+ ${renderError(modal.error)}
423
+ <div class="flex items-center justify-between gap-3 pt-2">
424
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
425
+ <button class="signature-button" type="submit" ${modal.submitting ? 'disabled' : ''}>
426
+ <span class="material-symbols-outlined text-sm">inventory_2</span>
427
+ ${modal.submitting ? 'Creating...' : 'Create backup'}
428
+ </button>
429
+ </div>
430
+ </form>
431
+ `;
432
+ }
433
+
434
+ function renderDeleteBackupForm(modal) {
435
+ return [
436
+ '<form class="space-y-5" data-form="delete-backup-confirm"><div class="space-y-3">',
437
+ '<p class="text-sm leading-6 text-on-surface-variant/70">Delete this managed backup and remove its backup file from disk.</p>',
423
438
  '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
424
- escapeHtml(modal.rowLabel),
425
- "</div>",
426
- ].join("")
427
- : "";
428
- const rowPreviewMarkup = rowPreview.length
429
- ? [
430
- '<div class="grid grid-cols-1 gap-3 md:grid-cols-2">',
431
- rowPreview
432
- .map((field) =>
433
- [
434
- '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">',
435
- '<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
436
- escapeHtml(field.label),
437
- "</div>",
438
- '<div class="mt-2 text-sm text-on-surface" title="',
439
- escapeHtml(field.fullValue ?? field.value ?? ""),
440
- '">',
441
- escapeHtml(field.value ?? ""),
442
- "</div></div>",
443
- ].join("")
444
- )
445
- .join(""),
446
- "</div>",
447
- ].join("")
448
- : "";
449
-
450
- return [
451
- '<form class="space-y-5" data-form="delete-row-confirm"><div class="space-y-3">',
452
- '<p class="text-sm leading-7 text-on-surface">Delete this row from <span class="font-bold text-primary-container">',
453
- escapeHtml(modal.tableName ?? "the current table"),
454
- "</span>?</p>",
455
- '<p class="text-sm leading-7 text-on-surface-variant/65">This action cannot be undone.</p>',
456
- rowLabelMarkup,
457
- rowPreviewMarkup,
458
- "</div>",
459
- renderError(modal.error),
460
- '<div class="flex items-center justify-between gap-3 pt-2">',
461
- '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
462
- '<button class="delete-button" type="submit">',
463
- modal.submitting ? "Deleting..." : "Delete Row",
464
- "</button></div></form>",
465
- ].join("");
439
+ escapeHtml(modal.backupName ?? 'Backup'),
440
+ modal.fileName ? ` // ${escapeHtml(modal.fileName)}` : '',
441
+ '</div></div>',
442
+ renderError(modal.error),
443
+ '<div class="flex items-center justify-between gap-3 pt-2">',
444
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
445
+ '<button class="delete-button" type="submit">',
446
+ '<span class="material-symbols-outlined text-sm">delete</span>',
447
+ modal.submitting ? 'Deleting...' : 'Delete Backup',
448
+ '</button></div></form>',
449
+ ].join('');
450
+ }
451
+
452
+ function renderEditBackupNotesForm(modal) {
453
+ return `
454
+ <form class="space-y-5" data-form="edit-backup-notes">
455
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
456
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Backup</div>
457
+ <div class="mt-2 font-headline text-sm font-black uppercase text-on-surface">
458
+ ${escapeHtml(modal.backupName ?? 'Backup')}
459
+ </div>
460
+ </div>
461
+ <label class="block space-y-2">
462
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Notes</span>
463
+ <textarea
464
+ class="control-input min-h-36 w-full resize-y border border-outline-variant/20 bg-surface-container-lowest px-3 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
465
+ name="notes"
466
+ placeholder="Optional backup notes" style="height: 160px"
467
+ >${escapeHtml(modal.notes ?? '')}</textarea>
468
+ </label>
469
+ ${renderError(modal.error)}
470
+ <div class="flex items-center justify-between gap-3 pt-2">
471
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
472
+ <button class="signature-button" type="submit" ${modal.submitting ? 'disabled' : ''}>
473
+ <span class="material-symbols-outlined text-sm">save</span>
474
+ ${modal.submitting ? 'Saving...' : 'Save notes'}
475
+ </button>
476
+ </div>
477
+ </form>
478
+ `;
479
+ }
480
+
481
+ function renderBackupSafetyForm(modal) {
482
+ const description =
483
+ modal.description ||
484
+ 'This operation may change or remove data or database structures. Creating a backup allows you to restore the current state if something goes wrong.';
485
+
486
+ return `
487
+ <div class="space-y-5">
488
+ <p class="text-sm leading-7 text-on-surface-variant/70">
489
+ ${escapeHtml(description)}
490
+ </p>
491
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
492
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Safety Backup</div>
493
+ <div class="mt-2 font-headline text-sm font-black uppercase text-on-surface">${escapeHtml(
494
+ modal.backupNameBeforeOperation ?? modal.backupName ?? 'Before operation',
495
+ )}</div>
496
+ </div>
497
+ ${renderError(modal.error)}
498
+ <div class="backup-safety-actions pt-2">
499
+ <button class="standard-button" data-action="backup-safety-cancel" type="button" ${modal.submitting ? 'disabled' : ''}>
500
+ Cancel
501
+ </button>
502
+ <button class="delete-button" data-action="backup-safety-continue" type="button" ${modal.submitting ? 'disabled' : ''}>
503
+ <span class="material-symbols-outlined text-sm">warning</span>
504
+ Continue without backup
505
+ </button>
506
+ <button class="signature-button" data-action="backup-safety-create" type="button" ${modal.submitting ? 'disabled' : ''}>
507
+ <span class="material-symbols-outlined text-sm">inventory_2</span>
508
+ ${modal.submitting ? 'Working...' : 'Create backup and continue'}
509
+ </button>
510
+ </div>
511
+ </div>
512
+ `;
513
+ }
514
+
515
+ function renderDeleteRowConfirmForm(modal) {
516
+ const rowPreview = modal.rowPreview ?? [];
517
+ const rowLabelMarkup = modal.rowLabel
518
+ ? [
519
+ '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
520
+ escapeHtml(modal.rowLabel),
521
+ '</div>',
522
+ ].join('')
523
+ : '';
524
+ const rowPreviewMarkup = rowPreview.length
525
+ ? [
526
+ '<div class="grid grid-cols-1 gap-3 md:grid-cols-2">',
527
+ rowPreview
528
+ .map(field =>
529
+ [
530
+ '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">',
531
+ '<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
532
+ escapeHtml(field.label),
533
+ '</div>',
534
+ '<div class="mt-2 text-sm text-on-surface" title="',
535
+ escapeHtml(field.fullValue ?? field.value ?? ''),
536
+ '">',
537
+ escapeHtml(field.value ?? ''),
538
+ '</div></div>',
539
+ ].join(''),
540
+ )
541
+ .join(''),
542
+ '</div>',
543
+ ].join('')
544
+ : '';
545
+
546
+ return [
547
+ '<form class="space-y-5" data-form="delete-row-confirm"><div class="space-y-3">',
548
+ '<p class="text-sm leading-7 text-on-surface">Delete this row from <span class="font-bold text-primary-container">',
549
+ escapeHtml(modal.tableName ?? 'the current table'),
550
+ '</span>?</p>',
551
+ '<p class="text-sm leading-7 text-on-surface-variant/65">This action cannot be undone.</p>',
552
+ rowLabelMarkup,
553
+ rowPreviewMarkup,
554
+ '</div>',
555
+ renderError(modal.error),
556
+ '<div class="flex items-center justify-between gap-3 pt-2">',
557
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
558
+ '<button class="delete-button" type="submit">',
559
+ modal.submitting ? 'Deleting...' : 'Delete Row',
560
+ '</button></div></form>',
561
+ ].join('');
466
562
  }
467
563
 
468
564
  function renderRowUpdatePreviewForm(modal) {
469
- const preview = modal.preview ?? {};
470
- const changes = preview.changes ?? [];
471
- const params = preview.params ?? [];
472
- const warnings = preview.warnings ?? [];
473
- const changesMarkup = changes.length
474
- ? [
475
- '<div class="overflow-hidden border border-outline-variant/10">',
476
- '<table class="w-full text-left text-sm">',
477
- '<thead class="bg-surface-container-highest text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">',
478
- '<tr><th class="px-3 py-2">Column</th><th class="px-3 py-2">Old</th><th class="px-3 py-2">New</th></tr>',
479
- '</thead><tbody class="divide-y divide-outline-variant/10">',
480
- changes
481
- .map(
482
- change => `
565
+ const preview = modal.preview ?? {};
566
+ const changes = preview.changes ?? [];
567
+ const params = preview.params ?? [];
568
+ const warnings = preview.warnings ?? [];
569
+ const changesMarkup = changes.length
570
+ ? [
571
+ '<div class="overflow-hidden border border-outline-variant/10">',
572
+ '<table class="w-full text-left text-sm">',
573
+ '<thead class="bg-surface-container-highest text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">',
574
+ '<tr><th class="px-3 py-2">Column</th><th class="px-3 py-2">Old</th><th class="px-3 py-2">New</th></tr>',
575
+ '</thead><tbody class="divide-y divide-outline-variant/10">',
576
+ changes
577
+ .map(
578
+ change => `
483
579
  <tr>
484
580
  <td class="px-3 py-2 font-mono text-xs text-primary-container">${escapeHtml(change.column)}</td>
485
581
  <td class="px-3 py-2 text-on-surface-variant/70">${escapeHtml(change.oldValue)}</td>
486
582
  <td class="px-3 py-2 text-on-surface">${escapeHtml(change.newValue)}</td>
487
583
  </tr>
488
584
  `,
489
- )
490
- .join(''),
491
- '</tbody></table></div>',
492
- ].join('')
493
- : '<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3 text-sm text-on-surface-variant/65">No changed values were detected.</div>';
494
- const paramsMarkup = params.length
495
- ? `
585
+ )
586
+ .join(''),
587
+ '</tbody></table></div>',
588
+ ].join('')
589
+ : '<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3 text-sm text-on-surface-variant/65">No changed values were detected.</div>';
590
+ const paramsMarkup = params.length
591
+ ? `
496
592
  <div class="space-y-2">
497
593
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">Parameters</div>
498
594
  <div class="space-y-2">
499
595
  ${params
500
- .map(
501
- param => `
596
+ .map(
597
+ param => `
502
598
  <div class="flex gap-3 border border-outline-variant/10 bg-surface-container-low px-3 py-2 text-xs">
503
599
  <span class="font-mono text-primary-container">$${escapeHtml(param.index)}</span>
504
600
  <span class="min-w-0 break-words text-on-surface">${escapeHtml(param.value)}</span>
505
601
  </div>
506
602
  `,
507
- )
508
- .join('')}
603
+ )
604
+ .join('')}
509
605
  </div>
510
606
  </div>
511
607
  `
512
- : '';
513
- const warningsMarkup = warnings.length
514
- ? `
608
+ : '';
609
+ const warningsMarkup = warnings.length
610
+ ? `
515
611
  <div class="space-y-2">
516
612
  ${warnings
517
- .map(
518
- warning => `
613
+ .map(
614
+ warning => `
519
615
  <div class="border border-primary-container/20 bg-primary-container/10 px-4 py-3 text-sm text-on-surface">
520
616
  ${escapeHtml(warning)}
521
617
  </div>
522
618
  `,
523
- )
524
- .join('')}
619
+ )
620
+ .join('')}
525
621
  </div>
526
622
  `
527
- : '';
623
+ : '';
528
624
 
529
- return `
625
+ return `
530
626
  <form class="space-y-5" data-form="apply-row-update-preview">
531
627
  <div class="space-y-2">
532
628
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
@@ -558,233 +654,233 @@ function renderRowUpdatePreviewForm(modal) {
558
654
  }
559
655
 
560
656
  function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
561
- const options = allowEmpty ? [{ value: "", label: "None" }] : [];
562
-
563
- return options.concat(
564
- (analysis?.columns ?? []).map((column) => ({
565
- value: column.name,
566
- label: `${column.name} (${column.type}${includeNumericHint && column.type === "number" ? " numeric" : ""})`,
567
- }))
568
- );
657
+ const options = allowEmpty ? [{ value: '', label: 'None' }] : [];
658
+
659
+ return options.concat(
660
+ (analysis?.columns ?? []).map(column => ({
661
+ value: column.name,
662
+ label: `${column.name} (${column.type}${includeNumericHint && column.type === 'number' ? ' numeric' : ''})`,
663
+ })),
664
+ );
569
665
  }
570
666
 
571
667
  function renderChartEditorForm(modal, state) {
572
- const draft = modal.draft ?? {};
573
- const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
574
- const chartTypeOptions = QUERY_CHART_TYPES.map((chartType) => ({
575
- value: chartType,
576
- label: getQueryChartTypeLabel(chartType),
577
- }));
578
- const columnOptions = renderChartColumnOptions(analysis);
579
- const optionalColumnOptions = renderChartColumnOptions(analysis, { allowEmpty: true });
580
- let chartSpecificFields = "";
581
-
582
- if (draft.chartType === "bar") {
583
- chartSpecificFields = `
668
+ const draft = modal.draft ?? {};
669
+ const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
670
+ const chartTypeOptions = QUERY_CHART_TYPES.map(chartType => ({
671
+ value: chartType,
672
+ label: getQueryChartTypeLabel(chartType),
673
+ }));
674
+ const columnOptions = renderChartColumnOptions(analysis);
675
+ const optionalColumnOptions = renderChartColumnOptions(analysis, { allowEmpty: true });
676
+ let chartSpecificFields = '';
677
+
678
+ if (draft.chartType === 'bar') {
679
+ chartSpecificFields = `
584
680
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
585
681
  ${renderSelectField({
586
- label: "X Column",
587
- value: draft.config?.x_column ?? "",
588
- options: columnOptions,
589
- bind: "query-chart-draft-config:x_column",
682
+ label: 'X Column',
683
+ value: draft.config?.x_column ?? '',
684
+ options: columnOptions,
685
+ bind: 'query-chart-draft-config:x_column',
590
686
  })}
591
687
  ${renderSelectField({
592
- label: "Y Column",
593
- value: draft.config?.y_column ?? "",
594
- options: columnOptions,
595
- bind: "query-chart-draft-config:y_column",
688
+ label: 'Y Column',
689
+ value: draft.config?.y_column ?? '',
690
+ options: columnOptions,
691
+ bind: 'query-chart-draft-config:y_column',
596
692
  })}
597
693
  </div>
598
694
  <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
599
695
  ${renderSelectField({
600
- label: "Sort By",
601
- value: draft.config?.sort_by ?? "y",
602
- options: [
603
- { value: "x", label: "X column" },
604
- { value: "y", label: "Y value" },
605
- ],
606
- bind: "query-chart-draft-config:sort_by",
696
+ label: 'Sort By',
697
+ value: draft.config?.sort_by ?? 'y',
698
+ options: [
699
+ { value: 'x', label: 'X column' },
700
+ { value: 'y', label: 'Y value' },
701
+ ],
702
+ bind: 'query-chart-draft-config:sort_by',
607
703
  })}
608
704
  ${renderSelectField({
609
- label: "Sort Direction",
610
- value: draft.config?.sort_direction ?? "desc",
611
- options: [
612
- { value: "asc", label: "Ascending / smallest first" },
613
- { value: "desc", label: "Descending / largest first" },
614
- ],
615
- bind: "query-chart-draft-config:sort_direction",
705
+ label: 'Sort Direction',
706
+ value: draft.config?.sort_direction ?? 'desc',
707
+ options: [
708
+ { value: 'asc', label: 'Ascending / smallest first' },
709
+ { value: 'desc', label: 'Descending / largest first' },
710
+ ],
711
+ bind: 'query-chart-draft-config:sort_direction',
616
712
  })}
617
713
  ${renderCheckboxField({
618
- label: "Show legend",
619
- name: "",
620
- checked: Boolean(draft.config?.show_legend),
621
- text: "Show legend",
622
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
714
+ label: 'Show legend',
715
+ name: '',
716
+ checked: Boolean(draft.config?.show_legend),
717
+ text: 'Show legend',
718
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_legend"')}
623
719
  ${renderCheckboxField({
624
- label: "Show labels",
625
- name: "",
626
- checked: Boolean(draft.config?.show_labels),
627
- text: "Show labels",
628
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
720
+ label: 'Show labels',
721
+ name: '',
722
+ checked: Boolean(draft.config?.show_labels),
723
+ text: 'Show labels',
724
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_labels"')}
629
725
  </div>
630
726
  `;
631
- } else if (draft.chartType === "line") {
632
- chartSpecificFields = `
727
+ } else if (draft.chartType === 'line') {
728
+ chartSpecificFields = `
633
729
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
634
730
  ${renderSelectField({
635
- label: "X Column",
636
- value: draft.config?.x_column ?? "",
637
- options: columnOptions,
638
- bind: "query-chart-draft-config:x_column",
731
+ label: 'X Column',
732
+ value: draft.config?.x_column ?? '',
733
+ options: columnOptions,
734
+ bind: 'query-chart-draft-config:x_column',
639
735
  })}
640
736
  ${renderSelectField({
641
- label: "Y Column",
642
- value: draft.config?.y_column ?? "",
643
- options: columnOptions,
644
- bind: "query-chart-draft-config:y_column",
737
+ label: 'Y Column',
738
+ value: draft.config?.y_column ?? '',
739
+ options: columnOptions,
740
+ bind: 'query-chart-draft-config:y_column',
645
741
  })}
646
742
  </div>
647
743
  <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
648
744
  ${renderSelectField({
649
- label: "Sort Direction",
650
- value: draft.config?.sort_direction ?? "asc",
651
- options: [
652
- { value: "asc", label: "Ascending" },
653
- { value: "desc", label: "Descending" },
654
- ],
655
- bind: "query-chart-draft-config:sort_direction",
745
+ label: 'Sort Direction',
746
+ value: draft.config?.sort_direction ?? 'asc',
747
+ options: [
748
+ { value: 'asc', label: 'Ascending' },
749
+ { value: 'desc', label: 'Descending' },
750
+ ],
751
+ bind: 'query-chart-draft-config:sort_direction',
656
752
  })}
657
753
  ${renderCheckboxField({
658
- label: "Smooth line",
659
- name: "",
660
- checked: Boolean(draft.config?.smooth),
661
- text: "Smooth line",
662
- }).replace("<input", '<input data-bind="query-chart-draft-config:smooth"')}
754
+ label: 'Smooth line',
755
+ name: '',
756
+ checked: Boolean(draft.config?.smooth),
757
+ text: 'Smooth line',
758
+ }).replace('<input', '<input data-bind="query-chart-draft-config:smooth"')}
663
759
  ${renderCheckboxField({
664
- label: "Show legend",
665
- name: "",
666
- checked: Boolean(draft.config?.show_legend),
667
- text: "Show legend",
668
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
760
+ label: 'Show legend',
761
+ name: '',
762
+ checked: Boolean(draft.config?.show_legend),
763
+ text: 'Show legend',
764
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_legend"')}
669
765
  ${renderCheckboxField({
670
- label: "Show labels",
671
- name: "",
672
- checked: Boolean(draft.config?.show_labels),
673
- text: "Show labels",
674
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
766
+ label: 'Show labels',
767
+ name: '',
768
+ checked: Boolean(draft.config?.show_labels),
769
+ text: 'Show labels',
770
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_labels"')}
675
771
  </div>
676
772
  `;
677
- } else if (draft.chartType === "pie") {
678
- chartSpecificFields = `
773
+ } else if (draft.chartType === 'pie') {
774
+ chartSpecificFields = `
679
775
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
680
776
  ${renderSelectField({
681
- label: "Label Column",
682
- value: draft.config?.label_column ?? "",
683
- options: columnOptions,
684
- bind: "query-chart-draft-config:label_column",
777
+ label: 'Label Column',
778
+ value: draft.config?.label_column ?? '',
779
+ options: columnOptions,
780
+ bind: 'query-chart-draft-config:label_column',
685
781
  })}
686
782
  ${renderSelectField({
687
- label: "Value Column",
688
- value: draft.config?.value_column ?? "",
689
- options: columnOptions,
690
- bind: "query-chart-draft-config:value_column",
783
+ label: 'Value Column',
784
+ value: draft.config?.value_column ?? '',
785
+ options: columnOptions,
786
+ bind: 'query-chart-draft-config:value_column',
691
787
  })}
692
788
  </div>
693
789
  <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
694
790
  ${renderCheckboxField({
695
- label: "Donut",
696
- name: "",
697
- checked: Boolean(draft.config?.donut),
698
- text: "Render as donut",
699
- }).replace("<input", '<input data-bind="query-chart-draft-config:donut"')}
791
+ label: 'Donut',
792
+ name: '',
793
+ checked: Boolean(draft.config?.donut),
794
+ text: 'Render as donut',
795
+ }).replace('<input', '<input data-bind="query-chart-draft-config:donut"')}
700
796
  ${renderCheckboxField({
701
- label: "Show legend",
702
- name: "",
703
- checked: Boolean(draft.config?.show_legend),
704
- text: "Show legend",
705
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
797
+ label: 'Show legend',
798
+ name: '',
799
+ checked: Boolean(draft.config?.show_legend),
800
+ text: 'Show legend',
801
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_legend"')}
706
802
  ${renderCheckboxField({
707
- label: "Show labels",
708
- name: "",
709
- checked: Boolean(draft.config?.show_labels),
710
- text: "Show labels",
711
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
803
+ label: 'Show labels',
804
+ name: '',
805
+ checked: Boolean(draft.config?.show_labels),
806
+ text: 'Show labels',
807
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_labels"')}
712
808
  </div>
713
809
  `;
714
- } else if (draft.chartType === "scatter") {
715
- chartSpecificFields = `
810
+ } else if (draft.chartType === 'scatter') {
811
+ chartSpecificFields = `
716
812
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
717
813
  ${renderSelectField({
718
- label: "X Column",
719
- value: draft.config?.x_column ?? "",
720
- options: columnOptions,
721
- bind: "query-chart-draft-config:x_column",
814
+ label: 'X Column',
815
+ value: draft.config?.x_column ?? '',
816
+ options: columnOptions,
817
+ bind: 'query-chart-draft-config:x_column',
722
818
  })}
723
819
  ${renderSelectField({
724
- label: "Y Column",
725
- value: draft.config?.y_column ?? "",
726
- options: columnOptions,
727
- bind: "query-chart-draft-config:y_column",
820
+ label: 'Y Column',
821
+ value: draft.config?.y_column ?? '',
822
+ options: columnOptions,
823
+ bind: 'query-chart-draft-config:y_column',
728
824
  })}
729
825
  </div>
730
826
  <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
731
827
  ${renderSelectField({
732
- label: "Size Column",
733
- value: draft.config?.size_column ?? "",
734
- options: optionalColumnOptions,
735
- bind: "query-chart-draft-config:size_column",
828
+ label: 'Size Column',
829
+ value: draft.config?.size_column ?? '',
830
+ options: optionalColumnOptions,
831
+ bind: 'query-chart-draft-config:size_column',
736
832
  })}
737
833
  ${renderSelectField({
738
- label: "Series Column",
739
- value: draft.config?.series_column ?? "",
740
- options: optionalColumnOptions,
741
- bind: "query-chart-draft-config:series_column",
834
+ label: 'Series Column',
835
+ value: draft.config?.series_column ?? '',
836
+ options: optionalColumnOptions,
837
+ bind: 'query-chart-draft-config:series_column',
742
838
  })}
743
839
  ${renderCheckboxField({
744
- label: "Show legend",
745
- name: "",
746
- checked: Boolean(draft.config?.show_legend),
747
- text: "Show legend",
748
- }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
840
+ label: 'Show legend',
841
+ name: '',
842
+ checked: Boolean(draft.config?.show_legend),
843
+ text: 'Show legend',
844
+ }).replace('<input', '<input data-bind="query-chart-draft-config:show_legend"')}
749
845
  </div>
750
846
  `;
751
- }
847
+ }
752
848
 
753
- return `
849
+ return `
754
850
  <form class="space-y-5" data-form="save-query-chart">
755
851
  ${renderField({
756
- label: "Chart Name",
757
- name: "chartName",
758
- value: draft.name ?? "",
759
- }).replace("<input", '<input data-bind="query-chart-draft:name"')}
852
+ label: 'Chart Name',
853
+ name: 'chartName',
854
+ value: draft.name ?? '',
855
+ }).replace('<input', '<input data-bind="query-chart-draft:name"')}
760
856
  ${renderSelectField({
761
- label: "Chart Type",
762
- value: draft.chartType ?? "bar",
763
- options: chartTypeOptions,
764
- bind: "query-chart-draft:chartType",
857
+ label: 'Chart Type',
858
+ value: draft.chartType ?? 'bar',
859
+ options: chartTypeOptions,
860
+ bind: 'query-chart-draft:chartType',
765
861
  })}
766
862
  ${chartSpecificFields}
767
863
  ${
768
- analysis
769
- ? `
864
+ analysis
865
+ ? `
770
866
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
771
867
  <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
772
868
  Result Columns
773
869
  </div>
774
870
  <div class="mt-3 flex flex-wrap gap-2">
775
871
  ${analysis.columns
776
- .map(
777
- (column) => `
872
+ .map(
873
+ column => `
778
874
  <span class="border border-outline-variant/15 bg-surface-container px-2 py-1 text-[10px] font-mono uppercase tracking-[0.12em] text-on-surface-variant/70">
779
875
  ${escapeHtml(column.name)} • ${escapeHtml(column.type)}
780
876
  </span>
781
- `
782
- )
783
- .join("")}
877
+ `,
878
+ )
879
+ .join('')}
784
880
  </div>
785
881
  </div>
786
882
  `
787
- : ""
883
+ : ''
788
884
  }
789
885
  ${renderError(modal.error)}
790
886
  <div class="flex items-center justify-between gap-3 pt-2">
@@ -799,7 +895,7 @@ function renderChartEditorForm(modal, state) {
799
895
  class="standard-button"
800
896
  type="submit"
801
897
  >
802
- ${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
898
+ ${modal.submitting ? 'Saving...' : draft.mode === 'edit' ? 'Save Chart' : 'Create Chart'}
803
899
  </button>
804
900
  </div>
805
901
  </form>
@@ -807,103 +903,103 @@ function renderChartEditorForm(modal, state) {
807
903
  }
808
904
 
809
905
  function renderDeleteChartForm(modal) {
810
- return [
811
- '<form class="space-y-5" data-form="delete-query-chart"><div class="space-y-3">',
812
- '<p class="text-sm leading-7 text-on-surface">Delete chart <span class="font-bold text-primary-container">',
813
- escapeHtml(modal.chartName ?? "Chart"),
814
- "</span>?</p>",
815
- '<p class="text-sm leading-7 text-on-surface-variant/65">The linked query-history entry stays intact. Only this chart definition is removed.</p>',
816
- "</div>",
817
- renderError(modal.error),
818
- '<div class="flex items-center justify-between gap-3 pt-2">',
819
- '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
820
- '<button class="delete-button" type="submit">',
821
- modal.submitting ? "Deleting..." : "Delete Chart",
822
- "</button></div></form>",
823
- ].join("");
906
+ return [
907
+ '<form class="space-y-5" data-form="delete-query-chart"><div class="space-y-3">',
908
+ '<p class="text-sm leading-7 text-on-surface">Delete chart <span class="font-bold text-primary-container">',
909
+ escapeHtml(modal.chartName ?? 'Chart'),
910
+ '</span>?</p>',
911
+ '<p class="text-sm leading-7 text-on-surface-variant/65">The linked query-history entry stays intact. Only this chart definition is removed.</p>',
912
+ '</div>',
913
+ renderError(modal.error),
914
+ '<div class="flex items-center justify-between gap-3 pt-2">',
915
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
916
+ '<button class="delete-button" type="submit">',
917
+ modal.submitting ? 'Deleting...' : 'Delete Chart',
918
+ '</button></div></form>',
919
+ ].join('');
824
920
  }
825
921
 
826
922
  function renderDeleteQueryHistoryForm(modal) {
827
- return [
828
- '<form class="space-y-5" data-form="delete-query-history-confirm"><div class="space-y-3">',
829
- '<p class="text-sm leading-7 text-on-surface">Delete query <span class="font-bold text-primary-container">',
830
- escapeHtml(modal.queryTitle ?? "SQL query"),
831
- "</span>?</p>",
832
- '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the query-history entry and all recorded runs linked to it.</p>',
833
- "</div>",
834
- renderError(modal.error),
835
- '<div class="flex items-center justify-between gap-3 pt-2">',
836
- '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
837
- '<button class="delete-button" type="submit">',
838
- modal.submitting ? "Deleting..." : "Delete Query",
839
- "</button></div></form>",
840
- ].join("");
923
+ return [
924
+ '<form class="space-y-5" data-form="delete-query-history-confirm"><div class="space-y-3">',
925
+ '<p class="text-sm leading-7 text-on-surface">Delete query <span class="font-bold text-primary-container">',
926
+ escapeHtml(modal.queryTitle ?? 'SQL query'),
927
+ '</span>?</p>',
928
+ '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the query-history entry and all recorded runs linked to it.</p>',
929
+ '</div>',
930
+ renderError(modal.error),
931
+ '<div class="flex items-center justify-between gap-3 pt-2">',
932
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
933
+ '<button class="delete-button" type="submit">',
934
+ modal.submitting ? 'Deleting...' : 'Delete Query',
935
+ '</button></div></form>',
936
+ ].join('');
841
937
  }
842
938
 
843
939
  function renderDeleteDocumentForm(modal) {
844
- return [
845
- '<form class="space-y-5" data-form="delete-document-confirm"><div class="space-y-3">',
846
- '<p class="text-sm leading-7 text-on-surface">Delete document <span class="font-bold text-primary-container">',
847
- escapeHtml(modal.filename ?? "document"),
848
- "</span>?</p>",
849
- '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the Markdown document from the active database document folder.</p>',
850
- '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
851
- formatNumber(modal.contentLength ?? 0),
852
- " chars</div>",
853
- "</div>",
854
- renderError(modal.error),
855
- '<div class="flex items-center justify-between gap-3 pt-2">',
856
- '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
857
- '<button class="delete-button" type="submit">',
858
- modal.submitting ? "Deleting..." : "Delete Document",
859
- "</button></div></form>",
860
- ].join("");
940
+ return [
941
+ '<form class="space-y-5" data-form="delete-document-confirm"><div class="space-y-3">',
942
+ '<p class="text-sm leading-7 text-on-surface">Delete document <span class="font-bold text-primary-container">',
943
+ escapeHtml(modal.filename ?? 'document'),
944
+ '</span>?</p>',
945
+ '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the Markdown document from the active database document folder.</p>',
946
+ '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
947
+ formatNumber(modal.contentLength ?? 0),
948
+ ' chars</div>',
949
+ '</div>',
950
+ renderError(modal.error),
951
+ '<div class="flex items-center justify-between gap-3 pt-2">',
952
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
953
+ '<button class="delete-button" type="submit">',
954
+ modal.submitting ? 'Deleting...' : 'Delete Document',
955
+ '</button></div></form>',
956
+ ].join('');
861
957
  }
862
958
 
863
959
  export function renderDeleteApiTokenForm(modal) {
864
- return [
865
- '<form class="space-y-5" data-form="delete-api-token-confirm"><div class="space-y-3">',
866
- '<p class="text-sm leading-7 text-on-surface">Delete API token <span class="font-bold text-primary-container">',
867
- escapeHtml(modal.tokenName ?? "API token"),
868
- "</span>?</p>",
869
- '<p class="text-sm leading-7 text-on-surface-variant/65">Requests using this token will immediately lose access to <span class="font-semibold text-on-surface">',
870
- escapeHtml(modal.databaseLabel ?? "the active database"),
871
- ".</span></p>",
872
- '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-[10px] text-on-surface-variant/55">',
873
- escapeHtml(modal.tokenPrefix ?? ""),
874
- "...</div>",
875
- "</div>",
876
- renderError(modal.error),
877
- '<div class="flex items-center justify-between gap-3 pt-2">',
878
- '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
879
- '<button class="delete-button" type="submit">',
880
- modal.submitting ? "Deleting..." : "Delete Token",
881
- "</button></div></form>",
882
- ].join("");
960
+ return [
961
+ '<form class="space-y-5" data-form="delete-api-token-confirm"><div class="space-y-3">',
962
+ '<p class="text-sm leading-7 text-on-surface">Delete API token <span class="font-bold text-primary-container">',
963
+ escapeHtml(modal.tokenName ?? 'API token'),
964
+ '</span>?</p>',
965
+ '<p class="text-sm leading-7 text-on-surface-variant/65">Requests using this token will immediately lose access to <span class="font-semibold text-on-surface">',
966
+ escapeHtml(modal.databaseLabel ?? 'the active database'),
967
+ '.</span></p>',
968
+ '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-[10px] text-on-surface-variant/55">',
969
+ escapeHtml(modal.tokenPrefix ?? ''),
970
+ '...</div>',
971
+ '</div>',
972
+ renderError(modal.error),
973
+ '<div class="flex items-center justify-between gap-3 pt-2">',
974
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
975
+ '<button class="delete-button" type="submit">',
976
+ modal.submitting ? 'Deleting...' : 'Delete Token',
977
+ '</button></div></form>',
978
+ ].join('');
883
979
  }
884
980
 
885
981
  function getDocumentInsertQueryTitle(query) {
886
- return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
982
+ return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
887
983
  }
888
984
 
889
985
  function getSelectedDocumentInsertQuery(modal) {
890
- const selectedHistoryId = String(modal.selectedHistoryId ?? "");
986
+ const selectedHistoryId = String(modal.selectedHistoryId ?? '');
891
987
 
892
- return (modal.queries ?? []).find((query) => String(query.id) === selectedHistoryId) ?? null;
988
+ return (modal.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
893
989
  }
894
990
 
895
991
  function renderDocumentInsertQuerySelect(modal, emptyText) {
896
- const queries = modal.queries ?? [];
992
+ const queries = modal.queries ?? [];
897
993
 
898
- if (modal.loading) {
899
- return '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">Loading saved queries...</div>';
900
- }
994
+ if (modal.loading) {
995
+ return '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">Loading saved queries...</div>';
996
+ }
901
997
 
902
- if (!queries.length) {
903
- return `<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">${escapeHtml(emptyText)}</div>`;
904
- }
998
+ if (!queries.length) {
999
+ return `<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">${escapeHtml(emptyText)}</div>`;
1000
+ }
905
1001
 
906
- return `
1002
+ return `
907
1003
  <label class="block space-y-2">
908
1004
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
909
1005
  Saved Query
@@ -914,54 +1010,53 @@ function renderDocumentInsertQuerySelect(modal, emptyText) {
914
1010
  name="historyId"
915
1011
  >
916
1012
  ${queries
917
- .map(
918
- (query) => `
1013
+ .map(
1014
+ query => `
919
1015
  <option
920
1016
  value="${escapeHtml(query.id)}"
921
- ${String(query.id) === String(modal.selectedHistoryId) ? "selected" : ""}
1017
+ ${String(query.id) === String(modal.selectedHistoryId) ? 'selected' : ''}
922
1018
  >
923
1019
  ${escapeHtml(getDocumentInsertQueryTitle(query))}
924
1020
  </option>
925
- `
926
- )
927
- .join("")}
1021
+ `,
1022
+ )
1023
+ .join('')}
928
1024
  </select>
929
1025
  </label>
930
1026
  `;
931
1027
  }
932
1028
 
933
1029
  function renderDocumentInsertQueryPreview(query) {
934
- if (!query) {
935
- return "";
936
- }
1030
+ if (!query) {
1031
+ return '';
1032
+ }
937
1033
 
938
- return `
1034
+ return `
939
1035
  <div class="space-y-2">
940
1036
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
941
1037
  Query Preview
942
1038
  </div>
943
1039
  <pre class="max-h-44 overflow-auto border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-xs leading-6 text-on-surface-variant/75 custom-scrollbar">${escapeHtml(
944
- query.rawSql || query.previewSql || ""
1040
+ query.rawSql || query.previewSql || '',
945
1041
  )}</pre>
946
1042
  </div>
947
1043
  `;
948
1044
  }
949
1045
 
950
1046
  function renderDocumentInsertTableForm(modal) {
951
- const selectedQuery = getSelectedDocumentInsertQuery(modal);
952
- const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
953
- ? 'disabled aria-disabled="true"'
954
- : "";
1047
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
1048
+ const disabledAttribute =
1049
+ modal.loading || modal.submitting || !(modal.queries ?? []).length ? 'disabled aria-disabled="true"' : '';
955
1050
 
956
- return `
1051
+ return `
957
1052
  <form class="space-y-5" data-form="document-insert-table">
958
- ${renderDocumentInsertQuerySelect(modal, "No saved queries are available for this database.")}
1053
+ ${renderDocumentInsertQuerySelect(modal, 'No saved queries are available for this database.')}
959
1054
  ${renderDocumentInsertQueryPreview(selectedQuery)}
960
1055
  ${renderError(modal.error)}
961
1056
  <div class="flex items-center justify-between gap-3 pt-2">
962
1057
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
963
1058
  <button class="signature-button" type="submit" ${disabledAttribute}>
964
- ${modal.submitting ? "Inserting..." : "Insert Table"}
1059
+ ${modal.submitting ? 'Inserting...' : 'Insert Table'}
965
1060
  </button>
966
1061
  </div>
967
1062
  </form>
@@ -969,34 +1064,33 @@ function renderDocumentInsertTableForm(modal) {
969
1064
  }
970
1065
 
971
1066
  function renderDocumentInsertNoteForm(modal) {
972
- const selectedQuery = getSelectedDocumentInsertQuery(modal);
973
- const note = String(selectedQuery?.notes ?? "").trim();
974
- const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
975
- ? 'disabled aria-disabled="true"'
976
- : "";
1067
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
1068
+ const note = String(selectedQuery?.notes ?? '').trim();
1069
+ const disabledAttribute =
1070
+ modal.loading || modal.submitting || !(modal.queries ?? []).length ? 'disabled aria-disabled="true"' : '';
977
1071
 
978
- return `
1072
+ return `
979
1073
  <form class="space-y-5" data-form="document-insert-note">
980
- ${renderDocumentInsertQuerySelect(modal, "No saved queries with notes are available for this database.")}
1074
+ ${renderDocumentInsertQuerySelect(modal, 'No saved queries with notes are available for this database.')}
981
1075
  ${
982
- note
983
- ? `
1076
+ note
1077
+ ? `
984
1078
  <div class="space-y-2">
985
1079
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
986
1080
  Note Preview
987
1081
  </div>
988
1082
  <pre class="max-h-64 overflow-auto whitespace-pre-wrap border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm leading-6 text-on-surface custom-scrollbar">${escapeHtml(
989
- note
1083
+ note,
990
1084
  )}</pre>
991
1085
  </div>
992
1086
  `
993
- : ""
1087
+ : ''
994
1088
  }
995
1089
  ${renderError(modal.error)}
996
1090
  <div class="flex items-center justify-between gap-3 pt-2">
997
1091
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
998
1092
  <button class="signature-button" type="submit" ${disabledAttribute}>
999
- ${modal.submitting ? "Inserting..." : "Insert Note"}
1093
+ ${modal.submitting ? 'Inserting...' : 'Insert Note'}
1000
1094
  </button>
1001
1095
  </div>
1002
1096
  </form>
@@ -1004,48 +1098,48 @@ function renderDocumentInsertNoteForm(modal) {
1004
1098
  }
1005
1099
 
1006
1100
  function renderQueryExportPreview(lines = []) {
1007
- return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
1101
+ return lines.map(line => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join('');
1008
1102
  }
1009
1103
 
1010
1104
  function getExportOptions() {
1011
- return [
1012
- { label: "CSV", format: "csv", icon: "table_rows", meta: ".csv", preview: ["id,name", "1,Acme"] },
1013
- { label: "TSV", format: "tsv", icon: "view_column", meta: ".tsv", preview: ["id\tname", "1\tAcme"] },
1014
- {
1015
- label: "Markdown",
1016
- format: "md",
1017
- icon: "article",
1018
- meta: ".md",
1019
- preview: ["| id | name |", "| --- | --- |"],
1020
- },
1021
- {
1022
- label: "JSON",
1023
- format: "json",
1024
- icon: "data_object",
1025
- meta: ".json",
1026
- preview: ['[', ' {"id":1,"name":"Acme"}', ']'],
1027
- },
1028
- {
1029
- label: "Parquet",
1030
- format: "parquet",
1031
- icon: "dataset",
1032
- meta: ".parquet",
1033
- preview: ["columnar binary", "typed row groups"],
1034
- },
1035
- {
1036
- label: "Duplicate",
1037
- format: "table",
1038
- icon: "table_chart",
1039
- meta: "as table",
1040
- preview: ["columns inferred", "rows prefilled"],
1041
- },
1042
- ];
1105
+ return [
1106
+ { label: 'CSV', format: 'csv', icon: 'table_rows', meta: '.csv', preview: ['id,name', '1,Acme'] },
1107
+ { label: 'TSV', format: 'tsv', icon: 'view_column', meta: '.tsv', preview: ['id\tname', '1\tAcme'] },
1108
+ {
1109
+ label: 'Markdown',
1110
+ format: 'md',
1111
+ icon: 'article',
1112
+ meta: '.md',
1113
+ preview: ['| id | name |', '| --- | --- |'],
1114
+ },
1115
+ {
1116
+ label: 'JSON',
1117
+ format: 'json',
1118
+ icon: 'data_object',
1119
+ meta: '.json',
1120
+ preview: ['[', ' {"id":1,"name":"Acme"}', ']'],
1121
+ },
1122
+ {
1123
+ label: 'Parquet',
1124
+ format: 'parquet',
1125
+ icon: 'dataset',
1126
+ meta: '.parquet',
1127
+ preview: ['columnar binary', 'typed row groups'],
1128
+ },
1129
+ {
1130
+ label: 'Duplicate',
1131
+ format: 'table',
1132
+ icon: 'table_chart',
1133
+ meta: 'as table',
1134
+ preview: ['columns inferred', 'rows prefilled'],
1135
+ },
1136
+ ];
1043
1137
  }
1044
1138
 
1045
1139
  function renderTextExportModal(modal, action) {
1046
- const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
1140
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : '';
1047
1141
 
1048
- return `
1142
+ return `
1049
1143
  <div class="space-y-5" data-export-modal>
1050
1144
  <label class="block space-y-2">
1051
1145
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -1057,14 +1151,14 @@ function renderTextExportModal(modal, action) {
1057
1151
  name="filename"
1058
1152
  spellcheck="false"
1059
1153
  type="text"
1060
- value="${escapeHtml(modal.filename ?? "")}"
1154
+ value="${escapeHtml(modal.filename ?? '')}"
1061
1155
  ${disabledAttribute}
1062
1156
  />
1063
1157
  </label>
1064
1158
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
1065
1159
  ${getExportOptions()
1066
- .map(
1067
- (option) => `
1160
+ .map(
1161
+ option => `
1068
1162
  <button
1069
1163
  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"
1070
1164
  data-action="${escapeHtml(action)}"
@@ -1095,8 +1189,8 @@ function renderTextExportModal(modal, action) {
1095
1189
  </span>
1096
1190
  </button>
1097
1191
  `,
1098
- )
1099
- .join("")}
1192
+ )
1193
+ .join('')}
1100
1194
  </div>
1101
1195
  ${renderError(modal.error)}
1102
1196
  <div class="flex justify-start">
@@ -1109,47 +1203,49 @@ function renderTextExportModal(modal, action) {
1109
1203
  }
1110
1204
 
1111
1205
  function renderQueryExportModal(modal) {
1112
- return renderTextExportModal(modal, "export-query-format");
1206
+ return renderTextExportModal(modal, 'export-query-format');
1113
1207
  }
1114
1208
 
1115
1209
  function renderDataExportModal(modal) {
1116
- return renderTextExportModal(modal, "export-data-format");
1210
+ return renderTextExportModal(modal, 'export-data-format');
1117
1211
  }
1118
1212
 
1119
1213
  function getCopyColumnResult(state, modal) {
1120
- return modal.scope === "charts" ? state.charts.result : state.editor.result;
1214
+ return modal.scope === 'charts' ? state.charts.result : state.editor.result;
1121
1215
  }
1122
1216
 
1123
1217
  function renderCopyColumnPreview(modal, state) {
1124
- const result = getCopyColumnResult(state, modal);
1125
- const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1126
- const separator = Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
1127
- ? "\n"
1128
- : String(modal.separator ?? ",");
1129
- const wrapper = String(modal.wrapper ?? '"');
1130
- const preview = isMarkdownTodo
1131
- ? modal.editedText ?? buildCopyColumnText({
1132
- result,
1133
- columnName: modal.columnName,
1134
- copyMode: modal.copyMode,
1135
- separator: "\n",
1136
- wrapper,
1137
- }).text
1138
- : buildCopyColumnPreviewText({
1139
- result,
1140
- columnName: modal.columnName,
1141
- copyMode: modal.copyMode,
1142
- separator,
1143
- wrapper,
1144
- maxRows: 4,
1145
- });
1146
-
1147
- if (!preview && !isMarkdownTodo) {
1148
- return "";
1149
- }
1150
-
1151
- if (isMarkdownTodo) {
1152
- return `
1218
+ const result = getCopyColumnResult(state, modal);
1219
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1220
+ const separator =
1221
+ Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
1222
+ ? '\n'
1223
+ : String(modal.separator ?? ',');
1224
+ const wrapper = String(modal.wrapper ?? '"');
1225
+ const preview = isMarkdownTodo
1226
+ ? (modal.editedText ??
1227
+ buildCopyColumnText({
1228
+ result,
1229
+ columnName: modal.columnName,
1230
+ copyMode: modal.copyMode,
1231
+ separator: '\n',
1232
+ wrapper,
1233
+ }).text)
1234
+ : buildCopyColumnPreviewText({
1235
+ result,
1236
+ columnName: modal.columnName,
1237
+ copyMode: modal.copyMode,
1238
+ separator,
1239
+ wrapper,
1240
+ maxRows: 4,
1241
+ });
1242
+
1243
+ if (!preview && !isMarkdownTodo) {
1244
+ return '';
1245
+ }
1246
+
1247
+ if (isMarkdownTodo) {
1248
+ return `
1153
1249
  <label class="block space-y-2">
1154
1250
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1155
1251
  Editable Preview
@@ -1161,9 +1257,9 @@ function renderCopyColumnPreview(modal, state) {
1161
1257
  >${escapeHtml(preview)}</textarea>
1162
1258
  </label>
1163
1259
  `;
1164
- }
1260
+ }
1165
1261
 
1166
- return `
1262
+ return `
1167
1263
  <div class="space-y-2">
1168
1264
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1169
1265
  Preview
@@ -1174,15 +1270,15 @@ function renderCopyColumnPreview(modal, state) {
1174
1270
  }
1175
1271
 
1176
1272
  function renderCopyColumnLineBreaksField({ checked = false, disabled = false } = {}) {
1177
- return `
1273
+ return `
1178
1274
  <label class="block space-y-2">
1179
1275
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1180
1276
  Format
1181
1277
  </span>
1182
- <span class="standard-checkbox ${disabled ? "is-disabled" : ""}">
1278
+ <span class="standard-checkbox ${disabled ? 'is-disabled' : ''}">
1183
1279
  <input
1184
- ${checked ? "checked" : ""}
1185
- ${disabled ? "disabled" : ""}
1280
+ ${checked ? 'checked' : ''}
1281
+ ${disabled ? 'disabled' : ''}
1186
1282
  data-bind="copy-column-format-field"
1187
1283
  data-field="lineBreaks"
1188
1284
  name="lineBreaks"
@@ -1194,8 +1290,8 @@ function renderCopyColumnLineBreaksField({ checked = false, disabled = false } =
1194
1290
  `;
1195
1291
  }
1196
1292
 
1197
- function renderCopyColumnFormatField({ label, name, value = "" }) {
1198
- return `
1293
+ function renderCopyColumnFormatField({ label, name, value = '' }) {
1294
+ return `
1199
1295
  <label class="block space-y-2">
1200
1296
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1201
1297
  ${escapeHtml(label)}
@@ -1213,49 +1309,49 @@ function renderCopyColumnFormatField({ label, name, value = "" }) {
1213
1309
  }
1214
1310
 
1215
1311
  export function renderCopyColumnModal(modal, state) {
1216
- const result = getCopyColumnResult(state, modal);
1217
- const rows = result?.rows ?? [];
1218
- const valueCount = modal.copyMode === "first-10" ? Math.min(rows.length, 10) : rows.length;
1219
- const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
1220
- const exportMetadata = getCopyColumnExportMetadata(modal.copyMode);
1221
- const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1222
- const lineBreaks = isMarkdownTodo || Boolean(modal.lineBreaks);
1223
- const formatFieldsMarkup = isMarkdownTodo
1224
- ? renderCopyColumnLineBreaksField({
1225
- checked: true,
1226
- disabled: true,
1227
- })
1228
- : `
1312
+ const result = getCopyColumnResult(state, modal);
1313
+ const rows = result?.rows ?? [];
1314
+ const valueCount = modal.copyMode === 'first-10' ? Math.min(rows.length, 10) : rows.length;
1315
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : '';
1316
+ const exportMetadata = getCopyColumnExportMetadata(modal.copyMode);
1317
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1318
+ const lineBreaks = isMarkdownTodo || Boolean(modal.lineBreaks);
1319
+ const formatFieldsMarkup = isMarkdownTodo
1320
+ ? renderCopyColumnLineBreaksField({
1321
+ checked: true,
1322
+ disabled: true,
1323
+ })
1324
+ : `
1229
1325
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
1230
1326
  ${renderCopyColumnFormatField({
1231
- label: "Separator",
1232
- name: "separator",
1233
- value: modal.separator ?? ",",
1327
+ label: 'Separator',
1328
+ name: 'separator',
1329
+ value: modal.separator ?? ',',
1234
1330
  })}
1235
1331
  ${renderCopyColumnFormatField({
1236
- label: "Wrapper",
1237
- name: "wrapper",
1238
- value: modal.wrapper ?? '"',
1332
+ label: 'Wrapper',
1333
+ name: 'wrapper',
1334
+ value: modal.wrapper ?? '"',
1239
1335
  })}
1240
1336
  ${renderCopyColumnLineBreaksField({
1241
- checked: lineBreaks,
1337
+ checked: lineBreaks,
1242
1338
  })}
1243
1339
  </div>
1244
1340
  `;
1245
1341
 
1246
- return `
1342
+ return `
1247
1343
  <form class="space-y-5" data-form="copy-column">
1248
- <input name="scope" type="hidden" value="${escapeHtml(modal.scope ?? "editor")}" />
1249
- <input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ?? "")}" />
1250
- <input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ?? "column")}" />
1344
+ <input name="scope" type="hidden" value="${escapeHtml(modal.scope ?? 'editor')}" />
1345
+ <input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ?? '')}" />
1346
+ <input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ?? 'column')}" />
1251
1347
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
1252
1348
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1253
1349
  Column
1254
1350
  </div>
1255
1351
  <div class="mt-2 flex min-w-0 items-center justify-between gap-4">
1256
1352
  <code class="min-w-0 truncate font-mono text-sm text-primary-container" title="${escapeHtml(
1257
- modal.columnName ?? ""
1258
- )}">${escapeHtml(modal.columnName ?? "")}</code>
1353
+ modal.columnName ?? '',
1354
+ )}">${escapeHtml(modal.columnName ?? '')}</code>
1259
1355
  <span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/50">
1260
1356
  ${escapeHtml(getCopyColumnActionLabel(modal.copyMode))} · ${formatNumber(valueCount)}
1261
1357
  </span>
@@ -1281,11 +1377,11 @@ export function renderCopyColumnModal(modal, state) {
1281
1377
  value="export"
1282
1378
  ${disabledAttribute}
1283
1379
  >
1284
- ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1380
+ ${modal.submitting ? 'Working...' : `Export as ${exportMetadata.extension.toUpperCase()}`}
1285
1381
  </button>
1286
1382
  ${
1287
- isMarkdownTodo
1288
- ? `
1383
+ isMarkdownTodo
1384
+ ? `
1289
1385
  <button
1290
1386
  class="standard-button"
1291
1387
  name="intent"
@@ -1293,10 +1389,10 @@ export function renderCopyColumnModal(modal, state) {
1293
1389
  value="document"
1294
1390
  ${disabledAttribute}
1295
1391
  >
1296
- ${modal.submitting ? "Working..." : "Export to document folder"}
1392
+ ${modal.submitting ? 'Working...' : 'Export to document folder'}
1297
1393
  </button>
1298
1394
  `
1299
- : ""
1395
+ : ''
1300
1396
  }
1301
1397
  <button
1302
1398
  class="signature-button"
@@ -1305,7 +1401,7 @@ export function renderCopyColumnModal(modal, state) {
1305
1401
  value="copy"
1306
1402
  ${disabledAttribute}
1307
1403
  >
1308
- ${modal.submitting ? "Working..." : "Copy"}
1404
+ ${modal.submitting ? 'Working...' : 'Copy'}
1309
1405
  </button>
1310
1406
  </div>
1311
1407
  </div>
@@ -1314,59 +1410,60 @@ export function renderCopyColumnModal(modal, state) {
1314
1410
  }
1315
1411
 
1316
1412
  function getTableDesignerUniqueConstraintTypeLabel(constraint) {
1317
- if (constraint.partial) {
1318
- return "Partial unique index";
1319
- }
1413
+ if (constraint.partial) {
1414
+ return 'Partial unique index';
1415
+ }
1320
1416
 
1321
- if ((constraint.columns?.length ?? 0) > 1) {
1322
- return "Multi-column unique";
1323
- }
1417
+ if ((constraint.columns?.length ?? 0) > 1) {
1418
+ return 'Multi-column unique';
1419
+ }
1324
1420
 
1325
- return "Unique constraint";
1421
+ return 'Unique constraint';
1326
1422
  }
1327
1423
 
1328
1424
  function renderTableDesignerUniqueConstraintExpression(constraint) {
1329
- const expression = String(constraint.expression || constraint.sql || "").trim();
1425
+ const expression = String(constraint.expression || constraint.sql || '').trim();
1330
1426
 
1331
- if (expression) {
1332
- return expression;
1333
- }
1427
+ if (expression) {
1428
+ return expression;
1429
+ }
1334
1430
 
1335
- const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1336
- return columns.length ? `UNIQUE (${columns.join(", ")})` : "UNIQUE constraint";
1431
+ const columns = (constraint.columns ?? []).map(column => column.name).filter(Boolean);
1432
+ return columns.length ? `UNIQUE (${columns.join(', ')})` : 'UNIQUE constraint';
1337
1433
  }
1338
1434
 
1339
1435
  function renderTableDesignerCheckConstraintExpression(constraint) {
1340
- return String(constraint.expression || "").trim() || "CHECK constraint";
1436
+ return String(constraint.expression || '').trim() || 'CHECK constraint';
1341
1437
  }
1342
1438
 
1343
1439
  function normalizeTableDesignerConstraintColumnName(name) {
1344
- return String(name ?? "").trim().toLowerCase();
1440
+ return String(name ?? '')
1441
+ .trim()
1442
+ .toLowerCase();
1345
1443
  }
1346
1444
 
1347
1445
  function tableDesignerConstraintIncludesColumn(constraint, columnName) {
1348
- const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1446
+ const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1349
1447
 
1350
- if (!normalizedColumn) {
1351
- return false;
1352
- }
1448
+ if (!normalizedColumn) {
1449
+ return false;
1450
+ }
1353
1451
 
1354
- return (constraint.columns ?? []).some(
1355
- (constraintColumn) =>
1356
- normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn
1357
- );
1452
+ return (constraint.columns ?? []).some(
1453
+ constraintColumn => normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn,
1454
+ );
1358
1455
  }
1359
1456
 
1360
1457
  function renderTableDesignerUniqueConstraintEditor(constraint) {
1361
- const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1458
+ const columns = (constraint.columns ?? []).map(column => column.name).filter(Boolean);
1362
1459
 
1363
- return `
1460
+ return `
1364
1461
  <article class="table-designer-constraint-editor">
1365
1462
  <div class="table-designer-constraint-editor__header">
1366
1463
  <div class="min-w-0">
1367
1464
  <div class="table-designer-constraint-editor__type">
1368
1465
  ${escapeHtml(getTableDesignerUniqueConstraintTypeLabel(constraint))}
1369
- ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ""}
1466
+ ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ''}
1370
1467
  </div>
1371
1468
  <input
1372
1469
  class="table-designer-field table-designer-constraint-editor__name"
@@ -1376,19 +1473,19 @@ function renderTableDesignerUniqueConstraintEditor(constraint) {
1376
1473
  data-field="name"
1377
1474
  spellcheck="false"
1378
1475
  type="text"
1379
- value="${escapeHtml(constraint.name || "UNIQUE constraint")}"
1476
+ value="${escapeHtml(constraint.name || 'UNIQUE constraint')}"
1380
1477
  />
1381
1478
  </div>
1382
1479
  <div class="status-badge status-badge--muted">Rebuild</div>
1383
1480
  </div>
1384
1481
  ${
1385
- columns.length
1386
- ? `
1482
+ columns.length
1483
+ ? `
1387
1484
  <div class="table-designer-constraint__columns">
1388
- ${columns.map((column) => `<span>${escapeHtml(column)}</span>`).join("")}
1485
+ ${columns.map(column => `<span>${escapeHtml(column)}</span>`).join('')}
1389
1486
  </div>
1390
1487
  `
1391
- : ""
1488
+ : ''
1392
1489
  }
1393
1490
  <textarea
1394
1491
  class="table-designer-constraint-editor__sql custom-scrollbar"
@@ -1403,20 +1500,20 @@ function renderTableDesignerUniqueConstraintEditor(constraint) {
1403
1500
  }
1404
1501
 
1405
1502
  function renderTableDesignerCheckConstraintEditor(constraint) {
1406
- const columns = constraint.columns ?? [];
1407
- const allowedValues = columns.flatMap((column) =>
1408
- (column.allowedValues ?? []).map((value) => ({
1409
- columnName: column.name,
1410
- value,
1411
- }))
1412
- );
1413
-
1414
- return `
1503
+ const columns = constraint.columns ?? [];
1504
+ const allowedValues = columns.flatMap(column =>
1505
+ (column.allowedValues ?? []).map(value => ({
1506
+ columnName: column.name,
1507
+ value,
1508
+ })),
1509
+ );
1510
+
1511
+ return `
1415
1512
  <article class="table-designer-constraint-editor">
1416
1513
  <div class="table-designer-constraint-editor__header">
1417
1514
  <div class="min-w-0">
1418
1515
  <div class="table-designer-constraint-editor__type">
1419
- ${columns.length ? escapeHtml(columns.map((column) => column.name).join(", ")) : "Table check"}
1516
+ ${columns.length ? escapeHtml(columns.map(column => column.name).join(', ')) : 'Table check'}
1420
1517
  </div>
1421
1518
  <input
1422
1519
  class="table-designer-field table-designer-constraint-editor__name"
@@ -1426,25 +1523,25 @@ function renderTableDesignerCheckConstraintEditor(constraint) {
1426
1523
  data-field="name"
1427
1524
  spellcheck="false"
1428
1525
  type="text"
1429
- value="${escapeHtml(constraint.name || "CHECK constraint")}"
1526
+ value="${escapeHtml(constraint.name || 'CHECK constraint')}"
1430
1527
  />
1431
1528
  </div>
1432
1529
  <div class="status-badge status-badge--muted">Rebuild</div>
1433
1530
  </div>
1434
1531
  ${
1435
- allowedValues.length
1436
- ? `
1532
+ allowedValues.length
1533
+ ? `
1437
1534
  <div class="table-designer-constraint__values custom-scrollbar">
1438
1535
  ${allowedValues
1439
- .map(
1440
- (entry) => `
1536
+ .map(
1537
+ entry => `
1441
1538
  <span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
1442
- `
1443
- )
1444
- .join("")}
1539
+ `,
1540
+ )
1541
+ .join('')}
1445
1542
  </div>
1446
1543
  `
1447
- : ""
1544
+ : ''
1448
1545
  }
1449
1546
  <textarea
1450
1547
  class="table-designer-constraint-editor__sql custom-scrollbar"
@@ -1459,26 +1556,26 @@ function renderTableDesignerCheckConstraintEditor(constraint) {
1459
1556
  }
1460
1557
 
1461
1558
  function renderTableDesignerConstraintSection({ title, count, emptyText, body }) {
1462
- return `
1559
+ return `
1463
1560
  <section class="table-designer-constraints-modal__section">
1464
1561
  <div class="table-designer-constraints-modal__section-header">
1465
1562
  <div class="table-designer-constraints-modal__section-title">${escapeHtml(title)}</div>
1466
1563
  <div class="status-badge status-badge--muted">${formatNumber(count)}</div>
1467
1564
  </div>
1468
1565
  ${
1469
- count
1470
- ? `<div class="table-designer-constraints__list">${body}</div>`
1471
- : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
1566
+ count
1567
+ ? `<div class="table-designer-constraints__list">${body}</div>`
1568
+ : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
1472
1569
  }
1473
1570
  </section>
1474
1571
  `;
1475
1572
  }
1476
1573
 
1477
1574
  function renderTableDesignerConstraintsModal(modal, state) {
1478
- const draft = state.tableDesigner?.draft;
1575
+ const draft = state.tableDesigner?.draft;
1479
1576
 
1480
- if (!draft) {
1481
- return `
1577
+ if (!draft) {
1578
+ return `
1482
1579
  <div class="table-designer-constraints-modal__empty">
1483
1580
  No active table designer draft.
1484
1581
  </div>
@@ -1486,62 +1583,60 @@ function renderTableDesignerConstraintsModal(modal, state) {
1486
1583
  <button class="standard-button" data-action="close-modal" type="button">Close</button>
1487
1584
  </div>
1488
1585
  `;
1489
- }
1490
-
1491
- const selectedColumn = modal.columnId
1492
- ? (draft.columns ?? []).find((column) => column.id === modal.columnId)
1493
- : null;
1494
- const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? "").trim();
1495
- const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
1496
- const selectedColumnLabel = selectedColumnName || "Unnamed column";
1497
- const allUniqueConstraints = draft.mode === "edit" ? draft.uniqueConstraints ?? [] : [];
1498
- const allCheckConstraints = draft.mode === "edit" ? draft.checkConstraints ?? [] : [];
1499
- const uniqueConstraints = hasSelectedColumn
1500
- ? allUniqueConstraints.filter((constraint) =>
1501
- tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1502
- )
1503
- : allUniqueConstraints;
1504
- const checkConstraints = hasSelectedColumn
1505
- ? allCheckConstraints.filter((constraint) =>
1506
- tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1507
- )
1508
- : allCheckConstraints;
1509
- const totalCount = uniqueConstraints.length + checkConstraints.length;
1510
-
1511
- return `
1586
+ }
1587
+
1588
+ const selectedColumn = modal.columnId ? (draft.columns ?? []).find(column => column.id === modal.columnId) : null;
1589
+ const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? '').trim();
1590
+ const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
1591
+ const selectedColumnLabel = selectedColumnName || 'Unnamed column';
1592
+ const allUniqueConstraints = draft.mode === 'edit' ? (draft.uniqueConstraints ?? []) : [];
1593
+ const allCheckConstraints = draft.mode === 'edit' ? (draft.checkConstraints ?? []) : [];
1594
+ const uniqueConstraints = hasSelectedColumn
1595
+ ? allUniqueConstraints.filter(constraint =>
1596
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName),
1597
+ )
1598
+ : allUniqueConstraints;
1599
+ const checkConstraints = hasSelectedColumn
1600
+ ? allCheckConstraints.filter(constraint =>
1601
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName),
1602
+ )
1603
+ : allCheckConstraints;
1604
+ const totalCount = uniqueConstraints.length + checkConstraints.length;
1605
+
1606
+ return `
1512
1607
  <div class="table-designer-constraints-modal">
1513
1608
  <div class="table-designer-constraints-modal__summary">
1514
1609
  <div class="min-w-0">
1515
1610
  <div class="table-designer-constraints-modal__label">Table</div>
1516
- <code title="${escapeHtml(draft.tableName ?? "")}">${escapeHtml(draft.tableName ?? "")}</code>
1611
+ <code title="${escapeHtml(draft.tableName ?? '')}">${escapeHtml(draft.tableName ?? '')}</code>
1517
1612
  </div>
1518
1613
  ${
1519
- hasSelectedColumn
1520
- ? `
1614
+ hasSelectedColumn
1615
+ ? `
1521
1616
  <div class="min-w-0">
1522
1617
  <div class="table-designer-constraints-modal__label">Column</div>
1523
1618
  <code title="${escapeHtml(selectedColumnLabel)}">${escapeHtml(selectedColumnLabel)}</code>
1524
1619
  </div>
1525
1620
  `
1526
- : ""
1621
+ : ''
1527
1622
  }
1528
1623
  <div class="status-badge status-badge--primary">V2 · ${formatNumber(totalCount)}</div>
1529
1624
  </div>
1530
1625
  ${renderTableDesignerConstraintSection({
1531
- title: "Check constraints",
1532
- count: checkConstraints.length,
1533
- emptyText: hasSelectedColumn
1534
- ? "No check constraints detected for this column."
1535
- : "No check constraints detected.",
1536
- body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(""),
1626
+ title: 'Check constraints',
1627
+ count: checkConstraints.length,
1628
+ emptyText: hasSelectedColumn
1629
+ ? 'No check constraints detected for this column.'
1630
+ : 'No check constraints detected.',
1631
+ body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(''),
1537
1632
  })}
1538
1633
  ${renderTableDesignerConstraintSection({
1539
- title: hasSelectedColumn ? "Related unique constraints" : "Unique constraints",
1540
- count: uniqueConstraints.length,
1541
- emptyText: hasSelectedColumn
1542
- ? "No related multi-column or partial unique constraints detected."
1543
- : "No multi-column or partial unique constraints detected.",
1544
- body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(""),
1634
+ title: hasSelectedColumn ? 'Related unique constraints' : 'Unique constraints',
1635
+ count: uniqueConstraints.length,
1636
+ emptyText: hasSelectedColumn
1637
+ ? 'No related multi-column or partial unique constraints detected.'
1638
+ : 'No multi-column or partial unique constraints detected.',
1639
+ body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(''),
1545
1640
  })}
1546
1641
  <div class="flex items-center justify-between gap-3 pt-2">
1547
1642
  <button class="standard-button" data-action="close-modal" type="button">Close</button>
@@ -1551,10 +1646,10 @@ function renderTableDesignerConstraintsModal(modal, state) {
1551
1646
  }
1552
1647
 
1553
1648
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
1554
- const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
1555
- const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
1649
+ const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
1650
+ const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
1556
1651
 
1557
- return `
1652
+ return `
1558
1653
  <form class="space-y-5" data-form="create-media-tagging-mapping-table">
1559
1654
  <div class="space-y-3">
1560
1655
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -1570,14 +1665,14 @@ function renderCreateMediaTaggingMappingTableForm(modal, state) {
1570
1665
  </div>
1571
1666
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
1572
1667
  ${
1573
- mappingExists
1574
- ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} already exists in the active database.`
1575
- : `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} does not exist yet.`
1668
+ mappingExists
1669
+ ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} already exists in the active database.`
1670
+ : `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} does not exist yet.`
1576
1671
  }
1577
1672
  ${
1578
- readOnly && !mappingExists
1579
- ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
1580
- : ""
1673
+ readOnly && !mappingExists
1674
+ ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
1675
+ : ''
1581
1676
  }
1582
1677
  </div>
1583
1678
  </div>
@@ -1597,17 +1692,17 @@ function renderCreateMediaTaggingMappingTableForm(modal, state) {
1597
1692
  Close
1598
1693
  </button>
1599
1694
  ${
1600
- !mappingExists
1601
- ? `
1695
+ !mappingExists
1696
+ ? `
1602
1697
  <button
1603
1698
  class="standard-button"
1604
1699
  type="submit"
1605
- ${readOnly ? "disabled" : ""}
1700
+ ${readOnly ? 'disabled' : ''}
1606
1701
  >
1607
- ${modal.submitting ? "Creating..." : "Create Table"}
1702
+ ${modal.submitting ? 'Creating...' : 'Create Table'}
1608
1703
  </button>
1609
1704
  `
1610
- : ""
1705
+ : ''
1611
1706
  }
1612
1707
  </div>
1613
1708
  </form>
@@ -1615,10 +1710,10 @@ function renderCreateMediaTaggingMappingTableForm(modal, state) {
1615
1710
  }
1616
1711
 
1617
1712
  function renderCreateMediaTaggingTagTableForm(modal, state) {
1618
- const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
1619
- const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
1713
+ const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
1714
+ const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
1620
1715
 
1621
- return `
1716
+ return `
1622
1717
  <form class="space-y-5" data-form="create-media-tagging-tag-table">
1623
1718
  <div class="space-y-3">
1624
1719
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -1634,14 +1729,14 @@ function renderCreateMediaTaggingTagTableForm(modal, state) {
1634
1729
  </div>
1635
1730
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
1636
1731
  ${
1637
- tagTableExists
1638
- ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} already exists in the active database.`
1639
- : `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} does not exist yet.`
1732
+ tagTableExists
1733
+ ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} already exists in the active database.`
1734
+ : `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} does not exist yet.`
1640
1735
  }
1641
1736
  ${
1642
- readOnly && !tagTableExists
1643
- ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
1644
- : ""
1737
+ readOnly && !tagTableExists
1738
+ ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
1739
+ : ''
1645
1740
  }
1646
1741
  </div>
1647
1742
  </div>
@@ -1661,17 +1756,17 @@ function renderCreateMediaTaggingTagTableForm(modal, state) {
1661
1756
  Close
1662
1757
  </button>
1663
1758
  ${
1664
- !tagTableExists
1665
- ? `
1759
+ !tagTableExists
1760
+ ? `
1666
1761
  <button
1667
1762
  class="standard-button"
1668
1763
  type="submit"
1669
- ${readOnly ? "disabled" : ""}
1764
+ ${readOnly ? 'disabled' : ''}
1670
1765
  >
1671
- ${modal.submitting ? "Creating..." : "Create Table"}
1766
+ ${modal.submitting ? 'Creating...' : 'Create Table'}
1672
1767
  </button>
1673
1768
  `
1674
- : ""
1769
+ : ''
1675
1770
  }
1676
1771
  </div>
1677
1772
  </form>
@@ -1679,128 +1774,148 @@ function renderCreateMediaTaggingTagTableForm(modal, state) {
1679
1774
  }
1680
1775
 
1681
1776
  export function renderModal(state) {
1682
- const modal = state.modal;
1683
-
1684
- if (!modal) {
1685
- return "";
1686
- }
1687
-
1688
- const contentByKind = {
1689
- "open-connection": {
1690
- eyebrow: "Filesystem // Open existing SQLite database",
1691
- title: "Connect Database",
1692
- body: renderOpenConnectionForm(modal),
1693
- },
1694
- "create-connection": {
1695
- eyebrow: "Filesystem // Create a new SQLite database",
1696
- title: "Create Database",
1697
- body: renderCreateDatabaseForm(modal),
1698
- },
1699
- "import-sql": {
1700
- eyebrow: "Import // Execute SQL dump into SQLite",
1701
- title: "Import SQL Dump",
1702
- body: renderImportSqlForm(modal, state),
1703
- },
1704
- "edit-connection": {
1705
- eyebrow: "Registry // Update saved SQLite target",
1706
- title: "Edit Connection",
1707
- body: renderEditConnectionForm(modal),
1708
- },
1709
- "delete-row": {
1710
- eyebrow: "Mutation // Confirm row deletion",
1711
- title: "Delete Row",
1712
- body: renderDeleteRowConfirmForm(modal),
1713
- },
1714
- "row-update-preview": {
1715
- eyebrow: "Mutation // Review row update",
1716
- title: "Review Update",
1717
- body: renderRowUpdatePreviewForm(modal),
1718
- },
1719
- "chart-editor": {
1720
- eyebrow: "Charts // Configure query-based ECharts panel",
1721
- title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
1722
- body: renderChartEditorForm(modal, state),
1723
- },
1724
- "delete-chart": {
1725
- eyebrow: "Charts // Confirm chart deletion",
1726
- title: "Delete Chart",
1727
- body: renderDeleteChartForm(modal),
1728
- },
1729
- "delete-query-history": {
1730
- eyebrow: "History // Confirm query deletion",
1731
- title: "Delete Query",
1732
- body: renderDeleteQueryHistoryForm(modal),
1733
- },
1734
- "delete-document": {
1735
- eyebrow: "Documents // Confirm deletion",
1736
- title: "Delete Document",
1737
- body: renderDeleteDocumentForm(modal),
1738
- },
1739
- "delete-api-token": {
1740
- eyebrow: "Settings // Confirm token deletion",
1741
- title: "Delete API Token",
1742
- body: renderDeleteApiTokenForm(modal),
1743
- },
1744
- "document-insert-table": {
1745
- eyebrow: "Documents // Saved query output",
1746
- title: "Insert Table",
1747
- body: renderDocumentInsertTableForm(modal),
1748
- },
1749
- "document-insert-note": {
1750
- eyebrow: "Documents // Saved query notes",
1751
- title: "Insert Note",
1752
- body: renderDocumentInsertNoteForm(modal),
1753
- },
1754
- "query-export": {
1755
- eyebrow: "SQL Editor // Export query result",
1756
- title: "Export Query",
1757
- body: renderQueryExportModal(modal),
1758
- },
1759
- "data-export": {
1760
- eyebrow: "Data Browser // Export table data",
1761
- title: "Export Table",
1762
- body: renderDataExportModal(modal),
1763
- },
1764
- "copy-column": {
1765
- eyebrow: "Results // Copy or export column values",
1766
- title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? "Export Markdown Todo" : "Copy column",
1767
- body: renderCopyColumnModal(modal, state),
1768
- },
1769
- "table-designer-constraints": {
1770
- eyebrow: "Table Designer // Constraints",
1771
- title: "Checks",
1772
- body: renderTableDesignerConstraintsModal(modal, state),
1773
- },
1774
- "create-media-tagging-tag-table": {
1775
- eyebrow: "Media Tagging // Create default tag table",
1776
- title: "Create Tag Table",
1777
- body: renderCreateMediaTaggingTagTableForm(modal, state),
1778
- },
1779
- "create-media-tagging-mapping-table": {
1780
- eyebrow: "Media Tagging // Create default join table",
1781
- title: "Create Mapping Table",
1782
- body: renderCreateMediaTaggingMappingTableForm(modal, state),
1783
- },
1784
- };
1785
-
1786
- const config = contentByKind[modal.kind];
1787
-
1788
- if (!config) {
1789
- return "";
1790
- }
1791
-
1792
- return `
1777
+ const modal = state.modal;
1778
+
1779
+ if (!modal) {
1780
+ return '';
1781
+ }
1782
+
1783
+ const contentByKind = {
1784
+ 'open-connection': {
1785
+ eyebrow: 'Filesystem // Open existing SQLite database',
1786
+ title: 'Connect Database',
1787
+ body: renderOpenConnectionForm(modal),
1788
+ },
1789
+ 'create-connection': {
1790
+ eyebrow: 'Filesystem // Create a new SQLite database',
1791
+ title: 'Create Database',
1792
+ body: renderCreateDatabaseForm(modal),
1793
+ },
1794
+ 'import-sql': {
1795
+ eyebrow: 'Import // Execute SQL dump into SQLite',
1796
+ title: 'Import SQL Dump',
1797
+ body: renderImportSqlForm(modal, state),
1798
+ },
1799
+ 'create-backup': {
1800
+ eyebrow: 'Backups // Managed SQLite snapshot',
1801
+ title: 'Create Backup',
1802
+ body: renderCreateBackupForm(modal),
1803
+ },
1804
+ 'delete-backup': {
1805
+ eyebrow: 'Backups // Confirm deletion',
1806
+ title: 'Delete Backup',
1807
+ body: renderDeleteBackupForm(modal),
1808
+ },
1809
+ 'edit-backup-notes': {
1810
+ eyebrow: 'Backups // Notes',
1811
+ title: 'Edit Backup Notes',
1812
+ body: renderEditBackupNotesForm(modal),
1813
+ },
1814
+ 'backup-safety': {
1815
+ eyebrow: 'Backups // Safety check',
1816
+ title: 'Create a safety backup?',
1817
+ body: renderBackupSafetyForm(modal),
1818
+ },
1819
+ 'edit-connection': {
1820
+ eyebrow: 'Registry // Update saved SQLite target',
1821
+ title: 'Edit Connection',
1822
+ body: renderEditConnectionForm(modal),
1823
+ },
1824
+ 'delete-row': {
1825
+ eyebrow: 'Mutation // Confirm row deletion',
1826
+ title: 'Delete Row',
1827
+ body: renderDeleteRowConfirmForm(modal),
1828
+ },
1829
+ 'row-update-preview': {
1830
+ eyebrow: 'Mutation // Review row update',
1831
+ title: 'Review Update',
1832
+ body: renderRowUpdatePreviewForm(modal),
1833
+ },
1834
+ 'chart-editor': {
1835
+ eyebrow: 'Charts // Configure query-based ECharts panel',
1836
+ title: modal.draft?.mode === 'edit' ? 'Edit Chart' : 'New Chart',
1837
+ body: renderChartEditorForm(modal, state),
1838
+ },
1839
+ 'delete-chart': {
1840
+ eyebrow: 'Charts // Confirm chart deletion',
1841
+ title: 'Delete Chart',
1842
+ body: renderDeleteChartForm(modal),
1843
+ },
1844
+ 'delete-query-history': {
1845
+ eyebrow: 'History // Confirm query deletion',
1846
+ title: 'Delete Query',
1847
+ body: renderDeleteQueryHistoryForm(modal),
1848
+ },
1849
+ 'delete-document': {
1850
+ eyebrow: 'Documents // Confirm deletion',
1851
+ title: 'Delete Document',
1852
+ body: renderDeleteDocumentForm(modal),
1853
+ },
1854
+ 'delete-api-token': {
1855
+ eyebrow: 'Settings // Confirm token deletion',
1856
+ title: 'Delete API Token',
1857
+ body: renderDeleteApiTokenForm(modal),
1858
+ },
1859
+ 'document-insert-table': {
1860
+ eyebrow: 'Documents // Saved query output',
1861
+ title: 'Insert Table',
1862
+ body: renderDocumentInsertTableForm(modal),
1863
+ },
1864
+ 'document-insert-note': {
1865
+ eyebrow: 'Documents // Saved query notes',
1866
+ title: 'Insert Note',
1867
+ body: renderDocumentInsertNoteForm(modal),
1868
+ },
1869
+ 'query-export': {
1870
+ eyebrow: 'SQL Editor // Export query result',
1871
+ title: 'Export Query',
1872
+ body: renderQueryExportModal(modal),
1873
+ },
1874
+ 'data-export': {
1875
+ eyebrow: 'Data Browser // Export table data',
1876
+ title: 'Export Table',
1877
+ body: renderDataExportModal(modal),
1878
+ },
1879
+ 'copy-column': {
1880
+ eyebrow: 'Results // Copy or export column values',
1881
+ title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? 'Export Markdown Todo' : 'Copy column',
1882
+ body: renderCopyColumnModal(modal, state),
1883
+ },
1884
+ 'table-designer-constraints': {
1885
+ eyebrow: 'Table Designer // Constraints',
1886
+ title: 'Checks',
1887
+ body: renderTableDesignerConstraintsModal(modal, state),
1888
+ },
1889
+ 'create-media-tagging-tag-table': {
1890
+ eyebrow: 'Media Tagging // Create default tag table',
1891
+ title: 'Create Tag Table',
1892
+ body: renderCreateMediaTaggingTagTableForm(modal, state),
1893
+ },
1894
+ 'create-media-tagging-mapping-table': {
1895
+ eyebrow: 'Media Tagging // Create default join table',
1896
+ title: 'Create Mapping Table',
1897
+ body: renderCreateMediaTaggingMappingTableForm(modal, state),
1898
+ },
1899
+ };
1900
+
1901
+ const config = contentByKind[modal.kind];
1902
+
1903
+ if (!config) {
1904
+ return '';
1905
+ }
1906
+
1907
+ return `
1793
1908
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1794
1909
  <div class="w-full ${
1795
- modal.kind === "chart-editor" ||
1796
- modal.kind === "document-insert-table" ||
1797
- modal.kind === "document-insert-note" ||
1798
- modal.kind === "row-update-preview" ||
1799
- modal.kind === "table-designer-constraints"
1800
- ? "max-w-3xl"
1801
- : modal.kind === "query-export" || modal.kind === "data-export"
1802
- ? "max-w-4xl"
1803
- : "max-w-xl"
1910
+ modal.kind === 'chart-editor' ||
1911
+ modal.kind === 'document-insert-table' ||
1912
+ modal.kind === 'document-insert-note' ||
1913
+ modal.kind === 'row-update-preview' ||
1914
+ modal.kind === 'table-designer-constraints'
1915
+ ? 'max-w-3xl'
1916
+ : modal.kind === 'query-export' || modal.kind === 'data-export' || modal.kind === 'backup-safety'
1917
+ ? 'max-w-4xl'
1918
+ : 'max-w-xl'
1804
1919
  } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1805
1920
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
1806
1921
  <div>