sqlite-hub 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/js/app.js +91 -5
- package/frontend/js/components/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +52 -13
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/store.js +67 -1
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/views/backups.js +45 -17
- package/frontend/js/views/data.js +42 -16
- package/frontend/js/views/settings.js +1 -0
- package/frontend/js/views/structure.js +21 -9
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/components.css +5 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -0
- package/package.json +1 -1
- package/tests/backups-view.test.js +6 -0
- package/tests/dropdown-button.test.js +26 -0
- package/tests/email-preview.test.js +89 -0
- package/tests/markdown-documents.test.js +24 -0
- package/tests/row-editor-null-values.test.js +29 -0
- package/tests/settings-view.test.js +1 -0
- package/tests/structure-view.test.js +4 -0
package/frontend/js/app.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
exportCurrentQueryFormat,
|
|
47
47
|
getState,
|
|
48
48
|
initializeApp,
|
|
49
|
+
insertMarkdownIntoLastOpenDocument,
|
|
49
50
|
loadMoreQueryHistory,
|
|
50
51
|
openModal,
|
|
51
52
|
openOverviewInFinder,
|
|
@@ -239,6 +240,7 @@ let lastRenderedPanelMarkup = '';
|
|
|
239
240
|
let lastRenderedModalMarkup = '';
|
|
240
241
|
let lastRenderedToastMarkup = '';
|
|
241
242
|
let lastRenderedChartsHistorySignature = '';
|
|
243
|
+
let lastRenderedChartsDetailSignature = '';
|
|
242
244
|
let lastRenderedChartsCardSignature = '';
|
|
243
245
|
let lastRenderedPanelOpen = false;
|
|
244
246
|
let lastRenderedLockedRoute = false;
|
|
@@ -786,6 +788,27 @@ function buildChartsCardSignature(state) {
|
|
|
786
788
|
});
|
|
787
789
|
}
|
|
788
790
|
|
|
791
|
+
function buildChartsDetailSignature(state) {
|
|
792
|
+
if (state.route.name !== 'charts') {
|
|
793
|
+
return '';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const detail = state.charts.detail;
|
|
797
|
+
const historyVisible = state.charts.historyPanelVisible !== false || !state.charts.selectedHistoryId;
|
|
798
|
+
|
|
799
|
+
return JSON.stringify({
|
|
800
|
+
selectedHistoryId: state.charts.selectedHistoryId ?? null,
|
|
801
|
+
historyVisible,
|
|
802
|
+
detailLoading: Boolean(state.charts.detailLoading),
|
|
803
|
+
detailError: state.charts.detailError
|
|
804
|
+
? [state.charts.detailError.code ?? '', state.charts.detailError.message ?? '']
|
|
805
|
+
: null,
|
|
806
|
+
hasDetailItem: Boolean(detail?.item),
|
|
807
|
+
displayTitle: detail?.item?.displayTitle ?? '',
|
|
808
|
+
cards: buildChartsCardSignature(state),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
789
812
|
function syncChartsHistorySelectionUi(state) {
|
|
790
813
|
const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
|
|
791
814
|
const historyButtons = shellRefs.view.querySelectorAll(
|
|
@@ -1368,6 +1391,7 @@ function renderApp(state) {
|
|
|
1368
1391
|
const modalMarkup = renderModal(state);
|
|
1369
1392
|
const toastMarkup = renderToasts(state.toasts);
|
|
1370
1393
|
const chartsHistorySignature = buildChartsHistorySignature(state);
|
|
1394
|
+
const chartsDetailSignature = buildChartsDetailSignature(state);
|
|
1371
1395
|
const chartsCardSignature = buildChartsCardSignature(state);
|
|
1372
1396
|
const isLockedRoute = [
|
|
1373
1397
|
'editor',
|
|
@@ -1388,6 +1412,7 @@ function renderApp(state) {
|
|
|
1388
1412
|
const panelChanged = panel !== lastRenderedPanelMarkup;
|
|
1389
1413
|
const modalChanged = modalMarkup !== lastRenderedModalMarkup;
|
|
1390
1414
|
const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
|
|
1415
|
+
const chartsDetailChanged = chartsDetailSignature !== lastRenderedChartsDetailSignature;
|
|
1391
1416
|
const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
|
|
1392
1417
|
const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
|
|
1393
1418
|
const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
|
|
@@ -1436,12 +1461,17 @@ function renderApp(state) {
|
|
|
1436
1461
|
const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
|
|
1437
1462
|
|
|
1438
1463
|
if (historyPatched) {
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
1441
|
-
|
|
1464
|
+
if (chartsDetailChanged) {
|
|
1465
|
+
if (chartsCardsChanged) {
|
|
1466
|
+
teardownQueryChartRenderer();
|
|
1467
|
+
}
|
|
1442
1468
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1469
|
+
preservedChartsDom = !chartsCardsChanged;
|
|
1470
|
+
mainPatched = patchChartsDetailUi(state, { preserveCharts: preservedChartsDom });
|
|
1471
|
+
} else {
|
|
1472
|
+
preservedChartsDom = true;
|
|
1473
|
+
mainPatched = syncChartsHistorySelectionUi(state);
|
|
1474
|
+
}
|
|
1445
1475
|
}
|
|
1446
1476
|
|
|
1447
1477
|
if (!mainPatched) {
|
|
@@ -1542,6 +1572,7 @@ function renderApp(state) {
|
|
|
1542
1572
|
lastRenderedModalMarkup = modalMarkup;
|
|
1543
1573
|
lastRenderedToastMarkup = toastMarkup;
|
|
1544
1574
|
lastRenderedChartsHistorySignature = chartsHistorySignature;
|
|
1575
|
+
lastRenderedChartsDetailSignature = chartsDetailSignature;
|
|
1545
1576
|
lastRenderedChartsCardSignature = chartsCardSignature;
|
|
1546
1577
|
lastRenderedPanelOpen = panelOpen;
|
|
1547
1578
|
lastRenderedLockedRoute = isLockedRoute;
|
|
@@ -1567,6 +1598,22 @@ async function executeEditorQueryAndNavigate() {
|
|
|
1567
1598
|
router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
|
|
1568
1599
|
}
|
|
1569
1600
|
|
|
1601
|
+
function quoteSqlIdentifier(identifier) {
|
|
1602
|
+
return `"${String(identifier ?? '').replace(/"/g, '""')}"`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
async function openTableInSqlEditor(tableName) {
|
|
1606
|
+
const normalizedTableName = String(tableName ?? '').trim();
|
|
1607
|
+
|
|
1608
|
+
if (!normalizedTableName) {
|
|
1609
|
+
showToast('No table selected for SQL Editor.', 'alert');
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
setCurrentQuery(`SELECT * FROM ${quoteSqlIdentifier(normalizedTableName)};`);
|
|
1614
|
+
router.navigate('/editor');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1570
1617
|
function formatCurrentQuery() {
|
|
1571
1618
|
const currentQuery = getState().editor.sqlText ?? '';
|
|
1572
1619
|
const formattedQuery = formatSqlQuery(currentQuery);
|
|
@@ -2103,6 +2150,39 @@ function exportRowEditorJson() {
|
|
|
2103
2150
|
showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
|
|
2104
2151
|
}
|
|
2105
2152
|
|
|
2153
|
+
async function insertRowEditorJsonIntoDocument() {
|
|
2154
|
+
const payload = buildRowEditorJsonPayload(getState());
|
|
2155
|
+
|
|
2156
|
+
if (!payload) {
|
|
2157
|
+
showToast('No row is selected.', 'alert');
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
const markdown = ['```json', payload.text, '```'].join('\n');
|
|
2162
|
+
|
|
2163
|
+
try {
|
|
2164
|
+
const result = await insertMarkdownIntoLastOpenDocument(markdown);
|
|
2165
|
+
|
|
2166
|
+
if (!result.documentId) {
|
|
2167
|
+
showToast('Open a document before inserting row JSON.', 'alert');
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
if (!result.inserted) {
|
|
2172
|
+
showToast('Row JSON could not be inserted.', 'alert');
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (!result.saved) {
|
|
2177
|
+
scheduleDocumentAutosave(result.documentId);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
showToast(`Row from "${payload.label}" inserted into document.`, 'success');
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
showToast('Open a document before inserting row JSON.', 'alert');
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2106
2186
|
function exportCurrentDocumentMarkdown() {
|
|
2107
2187
|
const documents = getState().documents;
|
|
2108
2188
|
|
|
@@ -2319,6 +2399,9 @@ async function handleAction(actionNode) {
|
|
|
2319
2399
|
case 'export-row-editor-json':
|
|
2320
2400
|
exportRowEditorJson();
|
|
2321
2401
|
return;
|
|
2402
|
+
case 'insert-row-editor-json-into-document':
|
|
2403
|
+
await insertRowEditorJsonIntoDocument();
|
|
2404
|
+
return;
|
|
2322
2405
|
case 'open-modal':
|
|
2323
2406
|
openModal(actionNode.dataset.modal, {
|
|
2324
2407
|
columnId: actionNode.dataset.columnId,
|
|
@@ -2496,6 +2579,9 @@ async function handleAction(actionNode) {
|
|
|
2496
2579
|
case 'refresh-backups':
|
|
2497
2580
|
await refreshBackups();
|
|
2498
2581
|
return;
|
|
2582
|
+
case 'open-table-in-sql-editor':
|
|
2583
|
+
await openTableInSqlEditor(actionNode.dataset.tableName);
|
|
2584
|
+
return;
|
|
2499
2585
|
case 'open-restore-backup-modal':
|
|
2500
2586
|
openRestoreBackupModal(actionNode.dataset.backupId);
|
|
2501
2587
|
return;
|
|
@@ -147,7 +147,7 @@ export function renderQueryResultsPane(
|
|
|
147
147
|
? renderDataGrid({
|
|
148
148
|
columns,
|
|
149
149
|
rows: result.rows ?? [],
|
|
150
|
-
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
150
|
+
tableClass: "data-table min-w-full border-collapse text-left font-mono text-xs",
|
|
151
151
|
theadClass: "sticky top-0 z-10 bg-surface-container-highest text-on-surface",
|
|
152
152
|
tbodyClass: "divide-y divide-outline-variant/5",
|
|
153
153
|
getRowClass: (_, index) =>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
import { renderDropdownButton } from "./dropdownButton.js";
|
|
3
|
+
import { detectEmailValue } from "../utils/emailPreview.js";
|
|
2
4
|
import {
|
|
3
5
|
compactPathForDisplay,
|
|
4
6
|
detectFilePathValue,
|
|
@@ -49,6 +51,19 @@ function withUrlBadge(badges = [], url) {
|
|
|
49
51
|
return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function withEmailBadge(badges = [], email) {
|
|
55
|
+
if (!email) {
|
|
56
|
+
return badges;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hasEmailBadge = badges.some((badge) => {
|
|
60
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
61
|
+
return String(label ?? "").toUpperCase() === "EMAIL";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return hasEmailBadge ? badges : [...badges, { label: "EMAIL", tone: "email" }];
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
function getAllowedValues(field) {
|
|
53
68
|
const seen = new Set();
|
|
54
69
|
|
|
@@ -277,7 +292,11 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
277
292
|
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
278
293
|
const filePathPreview = getFieldFilePathPreview({ ...field, rawValue }, tableMeta);
|
|
279
294
|
const url = getUrlValue(value);
|
|
280
|
-
const
|
|
295
|
+
const email = detectEmailValue(rawValue);
|
|
296
|
+
const badges = withEmailBadge(
|
|
297
|
+
withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview),
|
|
298
|
+
email
|
|
299
|
+
);
|
|
281
300
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
282
301
|
const jsonPreview = formatJsonPreview(value);
|
|
283
302
|
const valueState = getRowEditorValueState(rawValue);
|
|
@@ -290,7 +309,9 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
290
309
|
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
291
310
|
${
|
|
292
311
|
url ? "data-row-editor-url-field" : ""
|
|
293
|
-
}
|
|
312
|
+
}
|
|
313
|
+
${email ? "data-row-editor-email-field" : ""}
|
|
314
|
+
>
|
|
294
315
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
295
316
|
<span>${escapeHtml(displayLabel)}</span>
|
|
296
317
|
${renderFieldBadgesWithCharacterCount(badges, rawValue)}
|
|
@@ -334,10 +355,11 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
334
355
|
const columnName = getFieldColumnName(field);
|
|
335
356
|
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
336
357
|
const url = getUrlValue(field.value);
|
|
358
|
+
const email = detectEmailValue(getFieldRawValue(field));
|
|
337
359
|
const allowedValues = getAllowedValues(field);
|
|
338
360
|
const filePathPreview = getFieldFilePathPreview(field, tableMeta);
|
|
339
361
|
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
340
|
-
const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
|
|
362
|
+
const badges = withEmailBadge(withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview), email);
|
|
341
363
|
const jsonPreview = formatJsonPreview(field.value);
|
|
342
364
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
343
365
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
@@ -351,6 +373,7 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
351
373
|
data-row-editor-initial-state="${escapeHtml(valueState)}"
|
|
352
374
|
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
353
375
|
${url ? "data-row-editor-url-field" : ""}
|
|
376
|
+
${email ? "data-row-editor-email-field" : ""}
|
|
354
377
|
>
|
|
355
378
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
356
379
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
@@ -427,6 +450,10 @@ function getFieldBadgeClassName(tone) {
|
|
|
427
450
|
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
428
451
|
}
|
|
429
452
|
|
|
453
|
+
if (tone === "email") {
|
|
454
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
455
|
+
}
|
|
456
|
+
|
|
430
457
|
if (tone === "check") {
|
|
431
458
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
432
459
|
}
|
|
@@ -488,16 +515,28 @@ export function renderRowEditorPanel({
|
|
|
488
515
|
].join("")
|
|
489
516
|
: "",
|
|
490
517
|
jsonActionsEnabled
|
|
491
|
-
?
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
518
|
+
? renderDropdownButton({
|
|
519
|
+
icon: "download",
|
|
520
|
+
label: "Export",
|
|
521
|
+
title: "Export row",
|
|
522
|
+
items: [
|
|
523
|
+
{
|
|
524
|
+
action: "copy-row-editor-json",
|
|
525
|
+
icon: "content_copy",
|
|
526
|
+
label: "Copy to clipboard",
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
action: "export-row-editor-json",
|
|
530
|
+
icon: "download",
|
|
531
|
+
label: "JSON file",
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
action: "insert-row-editor-json-into-document",
|
|
535
|
+
icon: "description",
|
|
536
|
+
label: "Markdown document",
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
})
|
|
501
540
|
: "",
|
|
502
541
|
canSubmit
|
|
503
542
|
? [
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
export function renderTopNav() {
|
|
2
|
-
|
|
2
|
+
return `
|
|
3
3
|
<div class="top-nav-shell">
|
|
4
4
|
<a class="top-nav-brand" href="#/">SQLite Hub</a>
|
|
5
5
|
<div class="top-nav-actions">
|
|
6
|
-
<button class="top-nav-icon" data-action="refresh-view" type="button" aria-label="Refresh">
|
|
7
|
-
<span class="material-symbols-outlined">refresh</span>
|
|
8
|
-
</button>
|
|
9
6
|
<button
|
|
10
7
|
class="top-nav-icon"
|
|
11
8
|
data-action="open-modal"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { renderDropdownButton } from "./dropdownButton.js";
|
|
2
|
+
|
|
3
|
+
function buildOpenItem({ disabled, icon, label, target, tableName }) {
|
|
4
|
+
return {
|
|
5
|
+
action: "navigate",
|
|
6
|
+
dataAttributes: {
|
|
7
|
+
to: target,
|
|
8
|
+
},
|
|
9
|
+
disabled,
|
|
10
|
+
icon,
|
|
11
|
+
label,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderWorkspaceOpenDropdown({
|
|
16
|
+
align = "right",
|
|
17
|
+
destinations = [],
|
|
18
|
+
disabled = false,
|
|
19
|
+
tableName = "",
|
|
20
|
+
} = {}) {
|
|
21
|
+
const safeTableName = String(tableName ?? "").trim();
|
|
22
|
+
const items = destinations.map((destination) => {
|
|
23
|
+
if (destination.key === "sql-editor") {
|
|
24
|
+
return {
|
|
25
|
+
action: "open-table-in-sql-editor",
|
|
26
|
+
dataAttributes: {
|
|
27
|
+
tableName: safeTableName,
|
|
28
|
+
},
|
|
29
|
+
disabled: disabled || !safeTableName,
|
|
30
|
+
icon: "terminal",
|
|
31
|
+
label: "SQL Editor",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return buildOpenItem({
|
|
36
|
+
disabled: disabled || !safeTableName,
|
|
37
|
+
icon: destination.icon,
|
|
38
|
+
label: destination.label,
|
|
39
|
+
target: destination.target(safeTableName),
|
|
40
|
+
tableName: safeTableName,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return renderDropdownButton({
|
|
45
|
+
align,
|
|
46
|
+
disabled: disabled || !safeTableName,
|
|
47
|
+
icon: "open_in_new",
|
|
48
|
+
items,
|
|
49
|
+
label: "Open",
|
|
50
|
+
title: safeTableName ? `Open ${safeTableName}` : "Open table",
|
|
51
|
+
});
|
|
52
|
+
}
|
package/frontend/js/store.js
CHANGED
|
@@ -82,6 +82,7 @@ let chartsLoadVersion = 0;
|
|
|
82
82
|
let chartsDetailLoadVersion = 0;
|
|
83
83
|
let mediaTaggingPreviewVersion = 0;
|
|
84
84
|
let documentsLoadVersion = 0;
|
|
85
|
+
let lastOpenDocumentId = null;
|
|
85
86
|
|
|
86
87
|
function readStoredDataPageSize(fallback = DEFAULT_DATA_PAGE_SIZE) {
|
|
87
88
|
try {
|
|
@@ -1003,6 +1004,10 @@ function applyCurrentDocument(document) {
|
|
|
1003
1004
|
state.documents.draftContent = document?.content ?? '';
|
|
1004
1005
|
state.documents.dirty = false;
|
|
1005
1006
|
state.documents.saveError = null;
|
|
1007
|
+
|
|
1008
|
+
if (document?.id) {
|
|
1009
|
+
lastOpenDocumentId = document.id;
|
|
1010
|
+
}
|
|
1006
1011
|
}
|
|
1007
1012
|
|
|
1008
1013
|
function upsertDocumentSummary(document) {
|
|
@@ -2425,6 +2430,10 @@ async function previewMediaTaggingDraft(options = {}) {
|
|
|
2425
2430
|
function invalidateDatabaseCaches(options = {}) {
|
|
2426
2431
|
const preserveDataBrowserState = options.preserveDataBrowserState === true;
|
|
2427
2432
|
|
|
2433
|
+
if (!preserveDataBrowserState) {
|
|
2434
|
+
lastOpenDocumentId = null;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2428
2437
|
state.overview.data = null;
|
|
2429
2438
|
state.dataBrowser.tables = [];
|
|
2430
2439
|
if (!preserveDataBrowserState) {
|
|
@@ -2932,7 +2941,7 @@ function buildDocumentMarkdownInsertion(currentContent, insertion, range = null)
|
|
|
2932
2941
|
return `${before}${prefix}${text}${suffix}${after}`;
|
|
2933
2942
|
}
|
|
2934
2943
|
|
|
2935
|
-
function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
2944
|
+
export function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
2936
2945
|
if (!state.documents.selectedId) {
|
|
2937
2946
|
return false;
|
|
2938
2947
|
}
|
|
@@ -2950,6 +2959,63 @@ function insertMarkdownIntoCurrentDocumentDraft(markdown, range = null) {
|
|
|
2950
2959
|
return true;
|
|
2951
2960
|
}
|
|
2952
2961
|
|
|
2962
|
+
export async function insertMarkdownIntoLastOpenDocument(markdown) {
|
|
2963
|
+
if (state.route.name === 'documents' && state.documents.selectedId) {
|
|
2964
|
+
const inserted = insertMarkdownIntoCurrentDocumentDraft(markdown);
|
|
2965
|
+
|
|
2966
|
+
return {
|
|
2967
|
+
documentId: state.documents.selectedId,
|
|
2968
|
+
inserted,
|
|
2969
|
+
saved: false,
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
const documentId = lastOpenDocumentId ?? state.documents.selectedId;
|
|
2974
|
+
|
|
2975
|
+
if (!documentId) {
|
|
2976
|
+
return {
|
|
2977
|
+
documentId: null,
|
|
2978
|
+
inserted: false,
|
|
2979
|
+
saved: false,
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
const response = await api.getDocument(documentId);
|
|
2984
|
+
const document = response.data ?? null;
|
|
2985
|
+
|
|
2986
|
+
if (!document?.id) {
|
|
2987
|
+
return {
|
|
2988
|
+
documentId: null,
|
|
2989
|
+
inserted: false,
|
|
2990
|
+
saved: false,
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const nextContent = buildDocumentMarkdownInsertion(document.content, markdown);
|
|
2995
|
+
|
|
2996
|
+
if (nextContent === document.content) {
|
|
2997
|
+
return {
|
|
2998
|
+
documentId: document.id,
|
|
2999
|
+
inserted: false,
|
|
3000
|
+
saved: false,
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
const updateResponse = await api.updateDocument(document.id, {
|
|
3005
|
+
content: nextContent,
|
|
3006
|
+
});
|
|
3007
|
+
const updatedDocument = updateResponse.data ?? document;
|
|
3008
|
+
|
|
3009
|
+
lastOpenDocumentId = updatedDocument.id;
|
|
3010
|
+
upsertDocumentSummary(updatedDocument);
|
|
3011
|
+
|
|
3012
|
+
return {
|
|
3013
|
+
documentId: updatedDocument.id,
|
|
3014
|
+
inserted: true,
|
|
3015
|
+
saved: true,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
2953
3019
|
function getDocumentInsertQueryTitle(query) {
|
|
2954
3020
|
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || 'Saved query';
|
|
2955
3021
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const EMAIL_PATTERN = /^[^\s@<>"']+@[^\s@<>"']+\.[^\s@<>"']+$/;
|
|
2
|
+
|
|
3
|
+
export function detectEmailValue(value) {
|
|
4
|
+
if (typeof value !== "string") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const text = value.trim();
|
|
9
|
+
|
|
10
|
+
if (!text || text.length > 320 || !EMAIL_PATTERN.test(text)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const atIndex = text.lastIndexOf("@");
|
|
15
|
+
const localPart = text.slice(0, atIndex);
|
|
16
|
+
const domain = text.slice(atIndex + 1);
|
|
17
|
+
|
|
18
|
+
if (!localPart || !domain || domain.startsWith(".") || domain.endsWith(".")) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
type: "email",
|
|
24
|
+
value: text,
|
|
25
|
+
localPart,
|
|
26
|
+
domain,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -83,8 +83,24 @@ function renderFallbackMarkdown(markdown = '') {
|
|
|
83
83
|
return html.join('\n');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function escapeMarkdownHtmlOutsideFences(markdown = '') {
|
|
87
|
+
const lines = String(markdown ?? '').split('\n');
|
|
88
|
+
let inFence = false;
|
|
89
|
+
|
|
90
|
+
return lines
|
|
91
|
+
.map(line => {
|
|
92
|
+
if (FENCED_CODE_PATTERN.test(line)) {
|
|
93
|
+
inFence = !inFence;
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return inFence ? line : escapeHtml(line);
|
|
98
|
+
})
|
|
99
|
+
.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
function renderMarkdownBlock(markdown = '') {
|
|
87
|
-
const source =
|
|
103
|
+
const source = escapeMarkdownHtmlOutsideFences(markdown);
|
|
88
104
|
const markedRuntime = globalThis.marked;
|
|
89
105
|
const parser = typeof markedRuntime?.parse === 'function' ? markedRuntime.parse.bind(markedRuntime) : null;
|
|
90
106
|
|
|
@@ -28,6 +28,29 @@ function isBackupBusy(backup, state) {
|
|
|
28
28
|
return Boolean(state.backups.operationLoading) || ['creating', 'verifying', 'restoring'].includes(backup.status);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function renderBackupMetadataItem(label, value, extraClass = '') {
|
|
32
|
+
return `
|
|
33
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-3 py-2 ${extraClass}">
|
|
34
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">${escapeHtml(label)}</div>
|
|
35
|
+
<div class="mt-1 font-mono text-[10px] uppercase tracking-[0.1em] text-on-surface/80">${escapeHtml(value)}</div>
|
|
36
|
+
</div>
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderBackupMetadata(backup) {
|
|
41
|
+
return `
|
|
42
|
+
<div class="grid min-w-[24rem] grid-cols-2 gap-2">
|
|
43
|
+
${renderBackupMetadataItem('Size', formatBytes(backup.sizeBytes))}
|
|
44
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-3 py-2">
|
|
45
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">Status</div>
|
|
46
|
+
<div class="mt-1">${renderBackupStatus(backup)}</div>
|
|
47
|
+
</div>
|
|
48
|
+
${renderBackupMetadataItem('SQLite Hub', backup.sqliteHubVersion ? `v${backup.sqliteHubVersion}` : 'n/a')}
|
|
49
|
+
${renderBackupMetadataItem('SQLite', backup.sqliteVersion ? `v${backup.sqliteVersion}` : 'n/a')}
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
function renderBackupRows(state) {
|
|
32
55
|
return state.backups.items
|
|
33
56
|
.map(backup => {
|
|
@@ -39,27 +62,34 @@ function renderBackupRows(state) {
|
|
|
39
62
|
|
|
40
63
|
return `
|
|
41
64
|
<tr class="border-b border-outline-variant/10 align-top">
|
|
42
|
-
<td class="px-4 py-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
<td class="min-w-[20rem] px-4 py-5">
|
|
66
|
+
<div class="space-y-2">
|
|
67
|
+
<div class="font-headline text-sm font-black uppercase text-on-surface">${escapeHtml(backup.name)}</div>
|
|
68
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/45" title="${escapeHtml(backup.path)}">
|
|
69
|
+
${escapeHtml(truncateMiddle(backup.fileName || backup.path, 48))}
|
|
70
|
+
</div>
|
|
71
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/55">
|
|
72
|
+
Created // ${escapeHtml(formatCompactDateTime(backup.createdAt))}
|
|
73
|
+
</div>
|
|
47
74
|
</div>
|
|
48
75
|
</td>
|
|
49
|
-
<td class="px-4 py-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
<td class="px-4 py-5">
|
|
77
|
+
${renderBackupMetadata(backup)}
|
|
78
|
+
</td>
|
|
79
|
+
<td class="min-w-[18rem] max-w-[24rem] px-4 py-5">
|
|
80
|
+
<div class="flex h-full flex-col items-start gap-3">
|
|
81
|
+
<div class="text-xs leading-6 text-on-surface-variant/70">
|
|
82
|
+
${backup.notes ? escapeHtml(backup.notes) : '<span class="text-on-surface-variant/35">No notes</span>'}
|
|
83
|
+
${backup.errorMessage ? `<div class="mt-2 text-error">${escapeHtml(backup.errorMessage)}</div>` : ''}
|
|
84
|
+
</div>
|
|
55
85
|
<button class="standard-button" data-action="open-edit-backup-notes-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canEditNotes ? '' : 'disabled'}>
|
|
56
86
|
<span class="material-symbols-outlined text-sm">edit_note</span>
|
|
57
87
|
Edit Notes
|
|
58
88
|
</button>
|
|
59
89
|
</div>
|
|
60
90
|
</td>
|
|
61
|
-
<td class="px-4 py-
|
|
62
|
-
<div class="flex flex-
|
|
91
|
+
<td class="px-4 py-5">
|
|
92
|
+
<div class="flex min-w-[10rem] flex-col items-stretch gap-2">
|
|
63
93
|
<button class="standard-button" data-action="open-restore-backup-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canRestore ? '' : 'disabled'}>
|
|
64
94
|
<span class="material-symbols-outlined text-sm">restore</span>
|
|
65
95
|
Restore
|
|
@@ -128,10 +158,8 @@ function renderBackupsBody(state) {
|
|
|
128
158
|
<table class="min-w-full border-collapse text-left">
|
|
129
159
|
<thead class="border-b border-outline-variant/10 bg-surface-container">
|
|
130
160
|
<tr class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
131
|
-
<th class="px-4 py-3 font-normal">
|
|
132
|
-
<th class="px-4 py-3 font-normal">
|
|
133
|
-
<th class="px-4 py-3 text-right font-normal">Size</th>
|
|
134
|
-
<th class="px-4 py-3 font-normal">Status</th>
|
|
161
|
+
<th class="px-4 py-3 font-normal">Backup</th>
|
|
162
|
+
<th class="px-4 py-3 font-normal">Metadata</th>
|
|
135
163
|
<th class="px-4 py-3 font-normal">Note</th>
|
|
136
164
|
<th class="px-4 py-3 text-right font-normal">Actions</th>
|
|
137
165
|
</tr>
|