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.
- package/README.md +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +110 -0
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +24 -15
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +1 -0
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +383 -37
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +176 -0
- package/frontend/js/views/charts.js +1 -1
- package/frontend/js/views/documents.js +184 -154
- package/frontend/js/views/structure.js +26 -24
- package/frontend/styles/components.css +128 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +33 -21
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +64 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +37 -2
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +75 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +24 -0
- package/tests/structure-view.test.js +56 -0
|
@@ -1,29 +1,25 @@
|
|
|
1
|
-
import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from
|
|
1
|
+
import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from '../utils/format.js';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from
|
|
9
|
-
import { renderConnectionLogo } from
|
|
10
|
-
import { renderTextInput } from
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
55
|
-
|
|
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) :
|
|
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
|
-
|
|
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 =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
if (!error) {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
139
126
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
label: 'Label',
|
|
166
|
+
name: 'label',
|
|
167
|
+
placeholder: 'Optional display name',
|
|
181
168
|
})}
|
|
182
169
|
${renderCheckboxField({
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 ?
|
|
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
|
-
|
|
195
|
+
const connection = modal.connection ?? {};
|
|
209
196
|
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 ?
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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">
|
|
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="
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
372
|
+
return `
|
|
388
373
|
<form class="space-y-5" data-form="import-sql">
|
|
389
374
|
${renderField({
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 ?
|
|
397
|
+
${modal.submitting ? 'Importing...' : 'Import SQL Dump'}
|
|
413
398
|
</button>
|
|
414
399
|
</div>
|
|
415
400
|
</form>
|
|
416
401
|
`;
|
|
417
402
|
}
|
|
418
403
|
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
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.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
'<
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
-
|
|
603
|
+
)
|
|
604
|
+
.join('')}
|
|
509
605
|
</div>
|
|
510
606
|
</div>
|
|
511
607
|
`
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
608
|
+
: '';
|
|
609
|
+
const warningsMarkup = warnings.length
|
|
610
|
+
? `
|
|
515
611
|
<div class="space-y-2">
|
|
516
612
|
${warnings
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
619
|
+
)
|
|
620
|
+
.join('')}
|
|
525
621
|
</div>
|
|
526
622
|
`
|
|
527
|
-
|
|
623
|
+
: '';
|
|
528
624
|
|
|
529
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}).replace(
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}).replace(
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}).replace(
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}).replace(
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
}).replace(
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}).replace(
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}).replace(
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}).replace(
|
|
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
|
-
|
|
715
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}).replace(
|
|
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
|
-
|
|
849
|
+
return `
|
|
754
850
|
<form class="space-y-5" data-form="save-query-chart">
|
|
755
851
|
${renderField({
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}).replace(
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
982
|
+
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
|
|
887
983
|
}
|
|
888
984
|
|
|
889
985
|
function getSelectedDocumentInsertQuery(modal) {
|
|
890
|
-
|
|
986
|
+
const selectedHistoryId = String(modal.selectedHistoryId ?? '');
|
|
891
987
|
|
|
892
|
-
|
|
988
|
+
return (modal.queries ?? []).find(query => String(query.id) === selectedHistoryId) ?? null;
|
|
893
989
|
}
|
|
894
990
|
|
|
895
991
|
function renderDocumentInsertQuerySelect(modal, emptyText) {
|
|
896
|
-
|
|
992
|
+
const queries = modal.queries ?? [];
|
|
897
993
|
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
1013
|
+
.map(
|
|
1014
|
+
query => `
|
|
919
1015
|
<option
|
|
920
1016
|
value="${escapeHtml(query.id)}"
|
|
921
|
-
${String(query.id) === String(modal.selectedHistoryId) ?
|
|
1017
|
+
${String(query.id) === String(modal.selectedHistoryId) ? 'selected' : ''}
|
|
922
1018
|
>
|
|
923
1019
|
${escapeHtml(getDocumentInsertQueryTitle(query))}
|
|
924
1020
|
</option>
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1021
|
+
`,
|
|
1022
|
+
)
|
|
1023
|
+
.join('')}
|
|
928
1024
|
</select>
|
|
929
1025
|
</label>
|
|
930
1026
|
`;
|
|
931
1027
|
}
|
|
932
1028
|
|
|
933
1029
|
function renderDocumentInsertQueryPreview(query) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1030
|
+
if (!query) {
|
|
1031
|
+
return '';
|
|
1032
|
+
}
|
|
937
1033
|
|
|
938
|
-
|
|
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
|
-
|
|
1040
|
+
query.rawSql || query.previewSql || '',
|
|
945
1041
|
)}</pre>
|
|
946
1042
|
</div>
|
|
947
1043
|
`;
|
|
948
1044
|
}
|
|
949
1045
|
|
|
950
1046
|
function renderDocumentInsertTableForm(modal) {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
: "";
|
|
1047
|
+
const selectedQuery = getSelectedDocumentInsertQuery(modal);
|
|
1048
|
+
const disabledAttribute =
|
|
1049
|
+
modal.loading || modal.submitting || !(modal.queries ?? []).length ? 'disabled aria-disabled="true"' : '';
|
|
955
1050
|
|
|
956
|
-
|
|
1051
|
+
return `
|
|
957
1052
|
<form class="space-y-5" data-form="document-insert-table">
|
|
958
|
-
${renderDocumentInsertQuerySelect(modal,
|
|
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 ?
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
1072
|
+
return `
|
|
979
1073
|
<form class="space-y-5" data-form="document-insert-note">
|
|
980
|
-
${renderDocumentInsertQuerySelect(modal,
|
|
1074
|
+
${renderDocumentInsertQuerySelect(modal, 'No saved queries with notes are available for this database.')}
|
|
981
1075
|
${
|
|
982
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
1101
|
+
return lines.map(line => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join('');
|
|
1008
1102
|
}
|
|
1009
1103
|
|
|
1010
1104
|
function getExportOptions() {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
1140
|
+
const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : '';
|
|
1047
1141
|
|
|
1048
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1206
|
+
return renderTextExportModal(modal, 'export-query-format');
|
|
1113
1207
|
}
|
|
1114
1208
|
|
|
1115
1209
|
function renderDataExportModal(modal) {
|
|
1116
|
-
|
|
1210
|
+
return renderTextExportModal(modal, 'export-data-format');
|
|
1117
1211
|
}
|
|
1118
1212
|
|
|
1119
1213
|
function getCopyColumnResult(state, modal) {
|
|
1120
|
-
|
|
1214
|
+
return modal.scope === 'charts' ? state.charts.result : state.editor.result;
|
|
1121
1215
|
}
|
|
1122
1216
|
|
|
1123
1217
|
function renderCopyColumnPreview(modal, state) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
1278
|
+
<span class="standard-checkbox ${disabled ? 'is-disabled' : ''}">
|
|
1183
1279
|
<input
|
|
1184
|
-
${checked ?
|
|
1185
|
-
${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
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1327
|
+
label: 'Separator',
|
|
1328
|
+
name: 'separator',
|
|
1329
|
+
value: modal.separator ?? ',',
|
|
1234
1330
|
})}
|
|
1235
1331
|
${renderCopyColumnFormatField({
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1332
|
+
label: 'Wrapper',
|
|
1333
|
+
name: 'wrapper',
|
|
1334
|
+
value: modal.wrapper ?? '"',
|
|
1239
1335
|
})}
|
|
1240
1336
|
${renderCopyColumnLineBreaksField({
|
|
1241
|
-
|
|
1337
|
+
checked: lineBreaks,
|
|
1242
1338
|
})}
|
|
1243
1339
|
</div>
|
|
1244
1340
|
`;
|
|
1245
1341
|
|
|
1246
|
-
|
|
1342
|
+
return `
|
|
1247
1343
|
<form class="space-y-5" data-form="copy-column">
|
|
1248
|
-
<input name="scope" type="hidden" value="${escapeHtml(modal.scope ??
|
|
1249
|
-
<input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ??
|
|
1250
|
-
<input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ??
|
|
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
|
-
|
|
1258
|
-
)}">${escapeHtml(modal.columnName ??
|
|
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 ?
|
|
1380
|
+
${modal.submitting ? 'Working...' : `Export as ${exportMetadata.extension.toUpperCase()}`}
|
|
1285
1381
|
</button>
|
|
1286
1382
|
${
|
|
1287
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1413
|
+
if (constraint.partial) {
|
|
1414
|
+
return 'Partial unique index';
|
|
1415
|
+
}
|
|
1320
1416
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1417
|
+
if ((constraint.columns?.length ?? 0) > 1) {
|
|
1418
|
+
return 'Multi-column unique';
|
|
1419
|
+
}
|
|
1324
1420
|
|
|
1325
|
-
|
|
1421
|
+
return 'Unique constraint';
|
|
1326
1422
|
}
|
|
1327
1423
|
|
|
1328
1424
|
function renderTableDesignerUniqueConstraintExpression(constraint) {
|
|
1329
|
-
|
|
1425
|
+
const expression = String(constraint.expression || constraint.sql || '').trim();
|
|
1330
1426
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1427
|
+
if (expression) {
|
|
1428
|
+
return expression;
|
|
1429
|
+
}
|
|
1334
1430
|
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
|
|
1436
|
+
return String(constraint.expression || '').trim() || 'CHECK constraint';
|
|
1341
1437
|
}
|
|
1342
1438
|
|
|
1343
1439
|
function normalizeTableDesignerConstraintColumnName(name) {
|
|
1344
|
-
|
|
1440
|
+
return String(name ?? '')
|
|
1441
|
+
.trim()
|
|
1442
|
+
.toLowerCase();
|
|
1345
1443
|
}
|
|
1346
1444
|
|
|
1347
1445
|
function tableDesignerConstraintIncludesColumn(constraint, columnName) {
|
|
1348
|
-
|
|
1446
|
+
const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
|
|
1349
1447
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1448
|
+
if (!normalizedColumn) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1353
1451
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
);
|
|
1452
|
+
return (constraint.columns ?? []).some(
|
|
1453
|
+
constraintColumn => normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn,
|
|
1454
|
+
);
|
|
1358
1455
|
}
|
|
1359
1456
|
|
|
1360
1457
|
function renderTableDesignerUniqueConstraintEditor(constraint) {
|
|
1361
|
-
|
|
1458
|
+
const columns = (constraint.columns ?? []).map(column => column.name).filter(Boolean);
|
|
1362
1459
|
|
|
1363
|
-
|
|
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 ||
|
|
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
|
-
|
|
1386
|
-
|
|
1482
|
+
columns.length
|
|
1483
|
+
? `
|
|
1387
1484
|
<div class="table-designer-constraint__columns">
|
|
1388
|
-
${columns.map(
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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(
|
|
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 ||
|
|
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
|
-
|
|
1436
|
-
|
|
1532
|
+
allowedValues.length
|
|
1533
|
+
? `
|
|
1437
1534
|
<div class="table-designer-constraint__values custom-scrollbar">
|
|
1438
1535
|
${allowedValues
|
|
1439
|
-
|
|
1440
|
-
|
|
1536
|
+
.map(
|
|
1537
|
+
entry => `
|
|
1441
1538
|
<span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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
|
-
|
|
1575
|
+
const draft = state.tableDesigner?.draft;
|
|
1479
1576
|
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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 ??
|
|
1611
|
+
<code title="${escapeHtml(draft.tableName ?? '')}">${escapeHtml(draft.tableName ?? '')}</code>
|
|
1517
1612
|
</div>
|
|
1518
1613
|
${
|
|
1519
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1649
|
+
const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
|
|
1650
|
+
const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
|
|
1556
1651
|
|
|
1557
|
-
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1695
|
+
!mappingExists
|
|
1696
|
+
? `
|
|
1602
1697
|
<button
|
|
1603
1698
|
class="standard-button"
|
|
1604
1699
|
type="submit"
|
|
1605
|
-
${readOnly ?
|
|
1700
|
+
${readOnly ? 'disabled' : ''}
|
|
1606
1701
|
>
|
|
1607
|
-
${modal.submitting ?
|
|
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
|
-
|
|
1619
|
-
|
|
1713
|
+
const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
|
|
1714
|
+
const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
|
|
1620
1715
|
|
|
1621
|
-
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
1759
|
+
!tagTableExists
|
|
1760
|
+
? `
|
|
1666
1761
|
<button
|
|
1667
1762
|
class="standard-button"
|
|
1668
1763
|
type="submit"
|
|
1669
|
-
${readOnly ?
|
|
1764
|
+
${readOnly ? 'disabled' : ''}
|
|
1670
1765
|
>
|
|
1671
|
-
${modal.submitting ?
|
|
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
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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>
|