sqlite-hub 0.12.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/frontend/js/api.js +4 -2
- package/frontend/js/app.js +632 -12
- package/frontend/js/components/modal.js +432 -3
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +101 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +227 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +0 -3
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/package.json +2 -3
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- package/fill.js +0 -526
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
import {
|
|
3
|
+
compactPathForDisplay,
|
|
4
|
+
detectFilePathValue,
|
|
5
|
+
getPathTypeLabel,
|
|
6
|
+
} from "../utils/filePathPreview.js";
|
|
7
|
+
import {
|
|
8
|
+
getTimestampPreviewForField,
|
|
9
|
+
isProtectedKeyColumn,
|
|
10
|
+
} from "../utils/timestampPreview.js";
|
|
11
|
+
import {
|
|
12
|
+
formatTextCellCharacterCount,
|
|
13
|
+
getTextCellCharacterCount,
|
|
14
|
+
} from "../utils/textCellStats.js";
|
|
2
15
|
|
|
3
16
|
const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
4
17
|
|
|
@@ -87,6 +100,19 @@ function withCheckBadge(badges = [], allowedValues = []) {
|
|
|
87
100
|
return hasCheckBadge ? badges : [...badges, { label: "CHECK", tone: "check" }];
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
function withFilePathBadge(badges = [], filePathPreview) {
|
|
104
|
+
if (!filePathPreview) {
|
|
105
|
+
return badges;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasPathBadge = badges.some((badge) => {
|
|
109
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
110
|
+
return String(label ?? "").toUpperCase() === "PATH";
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return hasPathBadge ? badges : [...badges, { label: "PATH", tone: "filepath" }];
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
function renderOpenUrlButton(url) {
|
|
91
117
|
if (!url) {
|
|
92
118
|
return "";
|
|
@@ -131,6 +157,9 @@ function renderAllowedValuesSelect(field, allowedValues) {
|
|
|
131
157
|
return `
|
|
132
158
|
<select
|
|
133
159
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
160
|
+
data-row-editor-initial-value="${escapeHtml(currentValue)}"
|
|
161
|
+
data-row-editor-text-source
|
|
162
|
+
data-row-editor-timestamp-source
|
|
134
163
|
name="field:${escapeHtml(field.name)}"
|
|
135
164
|
>
|
|
136
165
|
${options}
|
|
@@ -138,6 +167,117 @@ function renderAllowedValuesSelect(field, allowedValues) {
|
|
|
138
167
|
`;
|
|
139
168
|
}
|
|
140
169
|
|
|
170
|
+
function getFieldColumnName(field = {}) {
|
|
171
|
+
return String(field.sourceColumn ?? field.name ?? "");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getFieldRawValue(field = {}) {
|
|
175
|
+
return field.rawValue === undefined ? field.value : field.rawValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getFieldTimestampPreview(field = {}, tableMeta = {}) {
|
|
179
|
+
return getTimestampPreviewForField({
|
|
180
|
+
columnName: getFieldColumnName(field),
|
|
181
|
+
value: getFieldRawValue(field),
|
|
182
|
+
tableMeta,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderTimestampPreview(field = {}, tableMeta = {}) {
|
|
187
|
+
const preview = getFieldTimestampPreview(field, tableMeta);
|
|
188
|
+
|
|
189
|
+
if (preview.kind === "protected-key") {
|
|
190
|
+
return `
|
|
191
|
+
<div class="text-[11px] text-on-surface-variant/55">
|
|
192
|
+
Key column - shown as raw value
|
|
193
|
+
</div>
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return `
|
|
198
|
+
<div
|
|
199
|
+
class="text-[11px] text-on-surface-variant/70"
|
|
200
|
+
data-row-editor-timestamp-preview
|
|
201
|
+
${preview.kind === "timestamp" ? "" : "hidden"}
|
|
202
|
+
>
|
|
203
|
+
${
|
|
204
|
+
preview.kind === "timestamp"
|
|
205
|
+
? `Interpretiert als Datum: ${escapeHtml(preview.formatted)}`
|
|
206
|
+
: ""
|
|
207
|
+
}
|
|
208
|
+
</div>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderTextCharacterCountBadge(value) {
|
|
213
|
+
const count = getTextCellCharacterCount(value);
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
<span
|
|
217
|
+
class="border px-2 py-1 text-[9px] ${getFieldBadgeClassName("char-count")}"
|
|
218
|
+
data-row-editor-char-count
|
|
219
|
+
${count === null ? "hidden" : ""}
|
|
220
|
+
>
|
|
221
|
+
${count === null ? "" : escapeHtml(formatTextCellCharacterCount(count))}
|
|
222
|
+
</span>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderFieldBadgesWithCharacterCount(badges = [], value) {
|
|
227
|
+
const [typeBadge, ...remainingBadges] = badges;
|
|
228
|
+
|
|
229
|
+
return [
|
|
230
|
+
typeBadge ? renderFieldBadge(typeBadge) : "",
|
|
231
|
+
renderTextCharacterCountBadge(value),
|
|
232
|
+
...remainingBadges.map((badge) => renderFieldBadge(badge)),
|
|
233
|
+
].join("");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getFieldFilePathPreview(field = {}, tableMeta = {}) {
|
|
237
|
+
return detectFilePathValue(getFieldRawValue(field), getFieldColumnName(field), tableMeta);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderFilePathPreview(field = {}, tableMeta = {}) {
|
|
241
|
+
if (isProtectedKeyColumn(getFieldColumnName(field), tableMeta)) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const preview = getFieldFilePathPreview(field, tableMeta);
|
|
246
|
+
|
|
247
|
+
return `
|
|
248
|
+
<div
|
|
249
|
+
class="border border-outline-variant/10 bg-surface-container px-4 py-4 text-xs text-on-surface"
|
|
250
|
+
data-row-editor-filepath-preview
|
|
251
|
+
${preview ? "" : "hidden"}
|
|
252
|
+
>
|
|
253
|
+
<div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/75">
|
|
254
|
+
<span class="material-symbols-outlined text-sm">folder</span>
|
|
255
|
+
Interpretiert als Filepath
|
|
256
|
+
</div>
|
|
257
|
+
<div class="mt-3 grid gap-2 font-mono text-[11px] leading-5">
|
|
258
|
+
<div>
|
|
259
|
+
<span class="text-on-surface-variant/55">Filename:</span>
|
|
260
|
+
<span data-row-editor-filepath-filename>${escapeHtml(preview?.fileName ?? "N/A")}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div data-row-editor-filepath-directory-row ${preview?.directory ? "" : "hidden"}>
|
|
263
|
+
<span class="text-on-surface-variant/55">Directory:</span>
|
|
264
|
+
<span data-row-editor-filepath-directory title="${escapeHtml(preview?.directory ?? "")}">
|
|
265
|
+
${escapeHtml(preview?.directory ? compactPathForDisplay(preview.directory, 72) : "")}
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div>
|
|
269
|
+
<span class="text-on-surface-variant/55">Extension:</span>
|
|
270
|
+
<span data-row-editor-filepath-extension>${escapeHtml(preview?.extension ?? "N/A")}</span>
|
|
271
|
+
</div>
|
|
272
|
+
<div>
|
|
273
|
+
<span class="text-on-surface-variant/55">Type:</span>
|
|
274
|
+
<span data-row-editor-filepath-type>${escapeHtml(preview ? getPathTypeLabel(preview.pathType) : "N/A")}</span>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
141
281
|
function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
142
282
|
return `
|
|
143
283
|
<div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
|
|
@@ -151,44 +291,66 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
151
291
|
`;
|
|
152
292
|
}
|
|
153
293
|
|
|
154
|
-
function renderReadonlyField(
|
|
294
|
+
function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
295
|
+
const label = field.label ?? field.name;
|
|
296
|
+
const value = field.value;
|
|
297
|
+
const rawValue = getFieldRawValue(field);
|
|
298
|
+
const columnName = getFieldColumnName(field);
|
|
299
|
+
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
300
|
+
const filePathPreview = getFieldFilePathPreview({ ...field, rawValue }, tableMeta);
|
|
155
301
|
const url = getUrlValue(value);
|
|
156
|
-
const badges = withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url);
|
|
302
|
+
const badges = withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview);
|
|
157
303
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
158
304
|
const jsonPreview = getJsonPreview(value);
|
|
159
305
|
|
|
160
306
|
return `
|
|
161
|
-
<div
|
|
307
|
+
<div
|
|
308
|
+
class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3"
|
|
309
|
+
data-row-editor-column-name="${escapeHtml(columnName)}"
|
|
310
|
+
data-row-editor-field
|
|
311
|
+
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
312
|
+
${
|
|
162
313
|
url ? "data-row-editor-url-field" : ""
|
|
163
314
|
}>
|
|
164
315
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
165
316
|
<span>${escapeHtml(displayLabel)}</span>
|
|
166
|
-
${badges
|
|
317
|
+
${renderFieldBadgesWithCharacterCount(badges, rawValue)}
|
|
167
318
|
</div>
|
|
168
319
|
${
|
|
169
320
|
jsonPreview
|
|
170
321
|
? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
|
|
171
322
|
: `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
|
|
172
323
|
}
|
|
324
|
+
${renderTimestampPreview({ ...field, rawValue }, tableMeta)}
|
|
325
|
+
${renderFilePathPreview({ ...field, rawValue }, tableMeta)}
|
|
173
326
|
${renderOpenUrlButton(url)}
|
|
174
327
|
</div>
|
|
175
328
|
`;
|
|
176
329
|
}
|
|
177
330
|
|
|
178
|
-
function renderEditableField(field) {
|
|
331
|
+
function renderEditableField(field, tableMeta = {}) {
|
|
332
|
+
const columnName = getFieldColumnName(field);
|
|
333
|
+
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
179
334
|
const url = getUrlValue(field.value);
|
|
180
335
|
const allowedValues = getAllowedValues(field);
|
|
336
|
+
const filePathPreview = getFieldFilePathPreview(field, tableMeta);
|
|
181
337
|
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
182
|
-
const badges = withUrlBadge(baseBadges, url);
|
|
338
|
+
const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
|
|
183
339
|
const jsonPreview = getJsonPreview(field.value);
|
|
184
340
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
185
341
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
186
342
|
|
|
187
343
|
return `
|
|
188
|
-
<div
|
|
344
|
+
<div
|
|
345
|
+
class="block space-y-2"
|
|
346
|
+
data-row-editor-column-name="${escapeHtml(columnName)}"
|
|
347
|
+
data-row-editor-field
|
|
348
|
+
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
349
|
+
${url ? "data-row-editor-url-field" : ""}
|
|
350
|
+
>
|
|
189
351
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
190
352
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
191
|
-
${badges.
|
|
353
|
+
${renderFieldBadgesWithCharacterCount(badges, inputType === "number" ? null : field.value ?? "")}
|
|
192
354
|
</span>
|
|
193
355
|
${
|
|
194
356
|
jsonPreview
|
|
@@ -202,6 +364,8 @@ function renderEditableField(field) {
|
|
|
202
364
|
? `
|
|
203
365
|
<input
|
|
204
366
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
367
|
+
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
368
|
+
data-row-editor-timestamp-source
|
|
205
369
|
name="field:${escapeHtml(field.name)}"
|
|
206
370
|
step="${escapeHtml(numberStep)}"
|
|
207
371
|
type="number"
|
|
@@ -212,6 +376,9 @@ function renderEditableField(field) {
|
|
|
212
376
|
? `
|
|
213
377
|
<input
|
|
214
378
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
379
|
+
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
380
|
+
data-row-editor-text-source
|
|
381
|
+
data-row-editor-timestamp-source
|
|
215
382
|
name="field:${escapeHtml(field.name)}"
|
|
216
383
|
data-row-editor-url-input
|
|
217
384
|
spellcheck="false"
|
|
@@ -225,11 +392,16 @@ function renderEditableField(field) {
|
|
|
225
392
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
|
|
226
393
|
jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
|
|
227
394
|
}"
|
|
395
|
+
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
396
|
+
data-row-editor-text-source
|
|
397
|
+
data-row-editor-timestamp-source
|
|
228
398
|
name="field:${escapeHtml(field.name)}"
|
|
229
399
|
spellcheck="false"
|
|
230
400
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
231
401
|
`
|
|
232
402
|
}
|
|
403
|
+
${renderTimestampPreview(field, tableMeta)}
|
|
404
|
+
${renderFilePathPreview(field, tableMeta)}
|
|
233
405
|
</div>
|
|
234
406
|
`;
|
|
235
407
|
}
|
|
@@ -251,6 +423,14 @@ function getFieldBadgeClassName(tone) {
|
|
|
251
423
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
252
424
|
}
|
|
253
425
|
|
|
426
|
+
if (tone === "filepath") {
|
|
427
|
+
return "border-outline-variant/30 bg-surface-container-high text-on-surface";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (tone === "char-count") {
|
|
431
|
+
return "border-outline-variant/25 bg-surface-container text-on-surface-variant";
|
|
432
|
+
}
|
|
433
|
+
|
|
254
434
|
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
255
435
|
}
|
|
256
436
|
|
|
@@ -274,11 +454,13 @@ export function renderRowEditorPanel({
|
|
|
274
454
|
hiddenFields = [],
|
|
275
455
|
editableFields = [],
|
|
276
456
|
readonlyFields = [],
|
|
457
|
+
tableMeta = {},
|
|
277
458
|
disabledMessage = "",
|
|
278
459
|
saveError = null,
|
|
279
460
|
saving = false,
|
|
280
461
|
deleting = false,
|
|
281
462
|
reloadAction = "",
|
|
463
|
+
jsonActionsEnabled = false,
|
|
282
464
|
submitLabel = "Save",
|
|
283
465
|
deleteAction = "",
|
|
284
466
|
deleteRowIndex = null,
|
|
@@ -297,6 +479,18 @@ export function renderRowEditorPanel({
|
|
|
297
479
|
'" type="button">Reload</button>',
|
|
298
480
|
].join("")
|
|
299
481
|
: "",
|
|
482
|
+
jsonActionsEnabled
|
|
483
|
+
? [
|
|
484
|
+
'<button class="standard-button" data-action="copy-row-editor-json" type="button">',
|
|
485
|
+
'<span class="material-symbols-outlined text-sm">content_copy</span>',
|
|
486
|
+
"Copy as JSON",
|
|
487
|
+
"</button>",
|
|
488
|
+
'<button class="standard-button" data-action="export-row-editor-json" type="button">',
|
|
489
|
+
'<span class="material-symbols-outlined text-sm">download</span>',
|
|
490
|
+
"Export as JSON",
|
|
491
|
+
"</button>",
|
|
492
|
+
].join("")
|
|
493
|
+
: "",
|
|
300
494
|
canSubmit
|
|
301
495
|
? [
|
|
302
496
|
'<button class="standard-button" form="',
|
|
@@ -393,7 +587,7 @@ export function renderRowEditorPanel({
|
|
|
393
587
|
? `
|
|
394
588
|
<div class="space-y-4">
|
|
395
589
|
${editableFields
|
|
396
|
-
.map((field) => renderEditableField(field))
|
|
590
|
+
.map((field) => renderEditableField(field, tableMeta))
|
|
397
591
|
.join("")}
|
|
398
592
|
</div>
|
|
399
593
|
`
|
|
@@ -425,7 +619,7 @@ export function renderRowEditorPanel({
|
|
|
425
619
|
</div>
|
|
426
620
|
<div class="space-y-3">
|
|
427
621
|
${readonlyFields
|
|
428
|
-
.map((field) => renderReadonlyField(field
|
|
622
|
+
.map((field) => renderReadonlyField(field, tableMeta))
|
|
429
623
|
.join("")}
|
|
430
624
|
</div>
|
|
431
625
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml } from '../utils/format.js';
|
|
1
|
+
import { escapeHtml, formatCompactDateTime, truncateMiddle } from '../utils/format.js';
|
|
2
2
|
import { renderConnectionLogo } from './connectionLogo.js';
|
|
3
3
|
|
|
4
4
|
const sidebarItems = [
|
|
@@ -37,9 +37,107 @@ function getActiveSidebarKey(routeName) {
|
|
|
37
37
|
return routeName;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function getConnectionTimeValue(connection) {
|
|
41
|
+
const value = connection?.lastOpenedAt ?? connection?.lastModifiedAt ?? 0;
|
|
42
|
+
const time = new Date(value).getTime();
|
|
43
|
+
return Number.isFinite(time) ? time : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getQuickPickConnections(state) {
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
|
|
49
|
+
[state.connections.active, ...(state.connections.recent ?? [])].forEach(connection => {
|
|
50
|
+
if (!connection?.id || byId.has(connection.id)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
byId.set(connection.id, connection);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return [...byId.values()]
|
|
58
|
+
.sort((left, right) => getConnectionTimeValue(right) - getConnectionTimeValue(left))
|
|
59
|
+
.slice(0, 5);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderQuickPickCard(connection, activeConnectionId) {
|
|
63
|
+
const isActive = connection.id === activeConnectionId;
|
|
64
|
+
const label = connection.label || 'Untitled database';
|
|
65
|
+
const path = connection.path || '';
|
|
66
|
+
|
|
67
|
+
return `
|
|
68
|
+
<button
|
|
69
|
+
class="sidebar-db-picker-card ${isActive ? 'is-active' : ''}"
|
|
70
|
+
data-action="select-connection"
|
|
71
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
${renderConnectionLogo(connection, {
|
|
75
|
+
containerClass: 'sidebar-db-picker-card__mark overflow-hidden',
|
|
76
|
+
imageClassName: 'h-full w-full object-cover',
|
|
77
|
+
iconClassName: 'text-[15px]',
|
|
78
|
+
icon: 'database',
|
|
79
|
+
})}
|
|
80
|
+
<span class="sidebar-db-picker-card__body">
|
|
81
|
+
<span class="sidebar-db-picker-card__label" title="${escapeHtml(label)}">
|
|
82
|
+
${escapeHtml(label)}
|
|
83
|
+
</span>
|
|
84
|
+
<span class="sidebar-db-picker-card__path" title="${escapeHtml(path)}">
|
|
85
|
+
${escapeHtml(path ? truncateMiddle(path, 34) : 'path:n/a')}
|
|
86
|
+
</span>
|
|
87
|
+
<span class="sidebar-db-picker-card__meta">
|
|
88
|
+
${isActive ? 'ACTIVE' : escapeHtml(formatCompactDateTime(connection.lastOpenedAt))}
|
|
89
|
+
${connection.readOnly ? ' // READ_ONLY' : ''}
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
</button>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderConnectionQuickPicker(state) {
|
|
97
|
+
const activeConnection = state.connections.active;
|
|
98
|
+
const quickPicks = getQuickPickConnections(state);
|
|
99
|
+
const hasQuickPicks = quickPicks.length > 0;
|
|
100
|
+
|
|
101
|
+
return `
|
|
102
|
+
<details class="sidebar-db-picker">
|
|
103
|
+
<summary class="sidebar-footer-card" title="Switch active database">
|
|
104
|
+
${renderConnectionLogo(activeConnection, {
|
|
105
|
+
containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
|
|
106
|
+
imageClassName: 'h-full w-full object-cover',
|
|
107
|
+
iconClassName: 'text-[15px]',
|
|
108
|
+
icon: 'memory',
|
|
109
|
+
})}
|
|
110
|
+
<span class="min-w-0 flex-1">
|
|
111
|
+
<span class="block truncate text-[10px] font-bold text-on-surface">
|
|
112
|
+
${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
|
|
113
|
+
</span>
|
|
114
|
+
<span class="block text-[8px] text-on-surface-variant/60">
|
|
115
|
+
${activeConnection?.readOnly ? 'READ_ONLY' : activeConnection ? 'READ_WRITE' : 'SELECT_DATABASE'}
|
|
116
|
+
</span>
|
|
117
|
+
</span>
|
|
118
|
+
<span class="material-symbols-outlined sidebar-db-picker__chevron" aria-hidden="true">expand_less</span>
|
|
119
|
+
</summary>
|
|
120
|
+
<div class="sidebar-db-picker__panel">
|
|
121
|
+
<div class="sidebar-db-picker__header">
|
|
122
|
+
<span>Quick Picks</span>
|
|
123
|
+
<span>${escapeHtml(String(quickPicks.length))}/5</span>
|
|
124
|
+
</div>
|
|
125
|
+
${
|
|
126
|
+
hasQuickPicks
|
|
127
|
+
? `<div class="sidebar-db-picker__list">
|
|
128
|
+
${quickPicks
|
|
129
|
+
.map(connection => renderQuickPickCard(connection, activeConnection?.id))
|
|
130
|
+
.join('')}
|
|
131
|
+
</div>`
|
|
132
|
+
: `<div class="sidebar-db-picker__empty">No recent databases</div>`
|
|
133
|
+
}
|
|
134
|
+
</div>
|
|
135
|
+
</details>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
40
139
|
export function renderSidebar(state) {
|
|
41
140
|
const activeKey = getActiveSidebarKey(state.route.name);
|
|
42
|
-
const activeConnection = state.connections.active;
|
|
43
141
|
const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
|
|
44
142
|
|
|
45
143
|
return `
|
|
@@ -86,22 +184,7 @@ export function renderSidebar(state) {
|
|
|
86
184
|
.join('')}
|
|
87
185
|
</nav>
|
|
88
186
|
<div class="sidebar-footer">
|
|
89
|
-
|
|
90
|
-
${renderConnectionLogo(activeConnection, {
|
|
91
|
-
containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
|
|
92
|
-
imageClassName: 'h-full w-full object-cover',
|
|
93
|
-
iconClassName: 'text-[15px]',
|
|
94
|
-
icon: 'memory',
|
|
95
|
-
})}
|
|
96
|
-
<div class="min-w-0">
|
|
97
|
-
<p class="truncate text-[10px] font-bold text-on-surface">
|
|
98
|
-
${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
|
|
99
|
-
</p>
|
|
100
|
-
<p class="text-[8px] text-on-surface-variant/60">
|
|
101
|
-
${activeConnection?.readOnly ? 'READ_ONLY' : 'READ_WRITE'}
|
|
102
|
-
</p>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
187
|
+
${renderConnectionQuickPicker(state)}
|
|
105
188
|
</div>
|
|
106
189
|
`;
|
|
107
190
|
}
|
|
@@ -81,6 +81,54 @@ export function renderTableDesignerReferenceColumnOptions(
|
|
|
81
81
|
].join("");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function normalizeConstraintColumnName(name) {
|
|
85
|
+
return String(name ?? "").trim().toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function constraintIncludesColumn(constraint, columnName) {
|
|
89
|
+
const normalizedColumn = normalizeConstraintColumnName(columnName);
|
|
90
|
+
|
|
91
|
+
if (!normalizedColumn) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (constraint.columns ?? []).some(
|
|
96
|
+
(constraintColumn) => normalizeConstraintColumnName(constraintColumn.name) === normalizedColumn
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function countColumnCheckConstraints(column, draft) {
|
|
101
|
+
if (draft.mode !== "edit") {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (draft.checkConstraints ?? []).filter((constraint) =>
|
|
106
|
+
constraintIncludesColumn(constraint, column.name)
|
|
107
|
+
).length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderColumnCheckAction(column, draft) {
|
|
111
|
+
const checkCount = countColumnCheckConstraints(column, draft);
|
|
112
|
+
const columnName = column.name || "Unnamed column";
|
|
113
|
+
|
|
114
|
+
return `
|
|
115
|
+
<button
|
|
116
|
+
aria-label="Checks for ${escapeHtml(columnName)}"
|
|
117
|
+
class="standard-button table-designer-row-check-button"
|
|
118
|
+
data-action="open-modal"
|
|
119
|
+
data-column-id="${escapeHtml(column.id)}"
|
|
120
|
+
data-column-name="${escapeHtml(column.name)}"
|
|
121
|
+
data-modal="table-designer-constraints"
|
|
122
|
+
title="Checks for ${escapeHtml(columnName)}"
|
|
123
|
+
type="button"
|
|
124
|
+
>
|
|
125
|
+
<span class="material-symbols-outlined text-base">fact_check</span>
|
|
126
|
+
<span>Checks</span>
|
|
127
|
+
${checkCount ? `<span class="status-badge status-badge--muted">${escapeHtml(String(checkCount))}</span>` : ""}
|
|
128
|
+
</button>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
84
132
|
function renderColumnRow(column, draft, catalogTables) {
|
|
85
133
|
const typeOptions = draft.supportedTypes
|
|
86
134
|
.map((type) =>
|
|
@@ -162,10 +210,13 @@ function renderColumnRow(column, draft, catalogTables) {
|
|
|
162
210
|
'" data-field="referencesColumn">',
|
|
163
211
|
referenceColumnOptions,
|
|
164
212
|
"</select>",
|
|
213
|
+
'<div class="table-designer-row-actions">',
|
|
214
|
+
renderColumnCheckAction(column, draft),
|
|
165
215
|
'<button class="delete-button" data-action="remove-table-designer-column" data-column-id="',
|
|
166
216
|
columnId,
|
|
167
217
|
'" type="button"><span class="material-symbols-outlined text-base">delete</span><span>Remove</span></button>',
|
|
168
218
|
"</div>",
|
|
219
|
+
"</div>",
|
|
169
220
|
].join("");
|
|
170
221
|
}
|
|
171
222
|
|
|
@@ -183,7 +234,7 @@ function renderColumnGrid(draft, catalogTables) {
|
|
|
183
234
|
<div>Default</div>
|
|
184
235
|
<div>FK Table</div>
|
|
185
236
|
<div>FK Column</div>
|
|
186
|
-
<div
|
|
237
|
+
<div>Actions</div>
|
|
187
238
|
</div>
|
|
188
239
|
${visibleColumns.map((column) => renderColumnRow(column, draft, catalogTables)).join("")}
|
|
189
240
|
</div>
|
|
@@ -225,6 +276,10 @@ export function renderTableDesignerFeedback(draft, saveError) {
|
|
|
225
276
|
title: message,
|
|
226
277
|
}));
|
|
227
278
|
const warningItems = draft?.warnings ?? [];
|
|
279
|
+
const schemaNotes = warningItems.filter(
|
|
280
|
+
(item) => item.tone === "muted" && item.code !== "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT"
|
|
281
|
+
);
|
|
282
|
+
const alertWarnings = warningItems.filter((item) => item.tone !== "muted");
|
|
228
283
|
|
|
229
284
|
return [
|
|
230
285
|
saveError
|
|
@@ -233,10 +288,11 @@ export function renderTableDesignerFeedback(draft, saveError) {
|
|
|
233
288
|
<div class="table-designer-main__error-code">${escapeHtml(saveError.code)}</div>
|
|
234
289
|
<div class="table-designer-main__error-text">${escapeHtml(saveError.message)}</div>
|
|
235
290
|
</div>
|
|
236
|
-
|
|
291
|
+
`
|
|
237
292
|
: "",
|
|
238
293
|
renderWarningList(validationItems, "table-designer-banner is-validation", "Validation", "alert"),
|
|
239
|
-
renderWarningList(
|
|
294
|
+
renderWarningList(alertWarnings, "table-designer-banner is-warning", "Warnings", "alert"),
|
|
295
|
+
renderWarningList(schemaNotes, "table-designer-banner is-note", "Schema Notes", "muted"),
|
|
240
296
|
].join("");
|
|
241
297
|
}
|
|
242
298
|
|
|
@@ -305,7 +361,7 @@ export function renderTableDesignerEditor(state) {
|
|
|
305
361
|
<div class="table-designer-main__subtitle">
|
|
306
362
|
${escapeHtml(String(visibleColumns.length))} visible column${
|
|
307
363
|
visibleColumns.length === 1 ? "" : "s"
|
|
308
|
-
} // SQLite-safe operations only
|
|
364
|
+
} // Table Designer v2 // SQLite-safe operations only
|
|
309
365
|
</div>
|
|
310
366
|
</div>
|
|
311
367
|
<div class="table-designer-main__actions">
|
|
@@ -338,13 +394,15 @@ export function renderTableDesignerEditor(state) {
|
|
|
338
394
|
<div>
|
|
339
395
|
<div class="table-designer-main__section-title">Columns</div>
|
|
340
396
|
</div>
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
397
|
+
<div class="table-designer-main__section-actions">
|
|
398
|
+
<button
|
|
399
|
+
class="standard-button"
|
|
400
|
+
data-action="add-table-designer-column"
|
|
401
|
+
type="button"
|
|
402
|
+
>
|
|
403
|
+
+ Add Column
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
348
406
|
</div>
|
|
349
407
|
${renderColumnGrid(draft, catalogTables)}
|
|
350
408
|
</section>
|