sqlite-hub 0.3.1 → 0.4.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 -6
- package/changelog.md +12 -0
- package/{js → frontend/js}/api.js +7 -0
- package/{js → frontend/js}/app.js +87 -3
- package/{js → frontend/js}/components/connectionCard.js +8 -9
- package/frontend/js/components/connectionLogo.js +33 -0
- package/{js → frontend/js}/components/emptyState.js +25 -11
- package/{js → frontend/js}/components/modal.js +131 -0
- package/{js → frontend/js}/components/rowEditorPanel.js +63 -33
- package/{js → frontend/js}/components/sidebar.js +8 -3
- package/{js → frontend/js}/store.js +236 -1
- package/{js → frontend/js}/views/data.js +106 -9
- package/{js → frontend/js}/views/editor.js +4 -0
- package/{js → frontend/js}/views/structure.js +10 -12
- package/{styles → frontend/styles}/structure-graph.css +5 -10
- package/package.json +3 -3
- package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
- package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
- package/server/data/db_logos/.gitkeep +0 -0
- package/server/routes/connections.js +2 -0
- package/server/routes/data.js +14 -0
- package/server/server.js +29 -6
- package/server/services/sqlite/connectionManager.js +68 -33
- package/server/services/sqlite/dataBrowserService.js +30 -0
- package/server/services/storage/appStateStore.js +159 -20
- package/server/utils/appPaths.js +42 -18
- /package/{assets → frontend/assets}/images/logo.webp +0 -0
- /package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
- /package/{assets → frontend/assets}/images/logo_raw.png +0 -0
- /package/{assets → frontend/assets}/images/logo_small.webp +0 -0
- /package/{assets → frontend/assets}/mockups/connections.png +0 -0
- /package/{assets → frontend/assets}/mockups/data.png +0 -0
- /package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
- /package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
- /package/{assets → frontend/assets}/mockups/home.png +0 -0
- /package/{assets → frontend/assets}/mockups/overview.png +0 -0
- /package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
- /package/{assets → frontend/assets}/mockups/structure.png +0 -0
- /package/{index.html → frontend/index.html} +0 -0
- /package/{js → frontend/js}/components/actionBar.js +0 -0
- /package/{js → frontend/js}/components/appShell.js +0 -0
- /package/{js → frontend/js}/components/badges.js +0 -0
- /package/{js → frontend/js}/components/bottomTabs.js +0 -0
- /package/{js → frontend/js}/components/dataGrid.js +0 -0
- /package/{js → frontend/js}/components/metricCard.js +0 -0
- /package/{js → frontend/js}/components/pageHeader.js +0 -0
- /package/{js → frontend/js}/components/queryEditor.js +0 -0
- /package/{js → frontend/js}/components/queryResults.js +0 -0
- /package/{js → frontend/js}/components/statusBar.js +0 -0
- /package/{js → frontend/js}/components/structureGraph.js +0 -0
- /package/{js → frontend/js}/components/toast.js +0 -0
- /package/{js → frontend/js}/components/topNav.js +0 -0
- /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
- /package/{js → frontend/js}/router.js +0 -0
- /package/{js → frontend/js}/utils/format.js +0 -0
- /package/{js → frontend/js}/views/connections.js +0 -0
- /package/{js → frontend/js}/views/landing.js +0 -0
- /package/{js → frontend/js}/views/overview.js +0 -0
- /package/{js → frontend/js}/views/settings.js +0 -0
- /package/{styles → frontend/styles}/base.css +0 -0
- /package/{styles → frontend/styles}/components.css +0 -0
- /package/{styles → frontend/styles}/layout.css +0 -0
- /package/{styles → frontend/styles}/tokens.css +0 -0
- /package/{styles → frontend/styles}/views.css +0 -0
- /package/{data → server/data}/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sqlite-hub ⚡️
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
5
|
A focused local-first app for browsing, editing, and querying SQLite databases.
|
|
6
6
|
|
|
@@ -24,25 +24,25 @@ SQLite Hub keeps that workflow sharp:
|
|
|
24
24
|
|
|
25
25
|
### Structure view
|
|
26
26
|
|
|
27
|
-

|
|
27
|
+

|
|
28
28
|
|
|
29
29
|
Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
|
|
30
30
|
|
|
31
31
|
### Data browser
|
|
32
32
|
|
|
33
|
-

|
|
33
|
+

|
|
34
34
|
|
|
35
35
|
Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
|
|
36
36
|
|
|
37
37
|
### Row editing
|
|
38
38
|
|
|
39
|
-

|
|
39
|
+

|
|
40
40
|
|
|
41
41
|
Open one record, edit it in place, commit, continue.
|
|
42
42
|
|
|
43
43
|
### SQL editor
|
|
44
44
|
|
|
45
|
-

|
|
45
|
+

|
|
46
46
|
|
|
47
47
|
Write queries in a syntax-highlighted editor, inspect results in the same workflow, and export result sets as CSV.
|
|
48
48
|
|
|
@@ -69,6 +69,12 @@ brew install sqlite-hub
|
|
|
69
69
|
npm install -g sqlit-hub
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## alternative port
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
sqlit-hub --port:4174
|
|
76
|
+
```
|
|
77
|
+
|
|
72
78
|
## Changelog
|
|
73
79
|
|
|
74
|
-
[Changelog](changelog.md)
|
|
80
|
+
[Changelog](/changelog.md)
|
package/changelog.md
CHANGED
|
@@ -179,6 +179,13 @@ export function updateDataTableRow(tableName, payload) {
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export function deleteDataTableRow(tableName, payload) {
|
|
183
|
+
return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
|
|
184
|
+
method: "DELETE",
|
|
185
|
+
body: payload,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
182
189
|
export function getSettings() {
|
|
183
190
|
return request("/api/settings");
|
|
184
191
|
}
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
initializeApp,
|
|
27
27
|
loadQueryFromHistory,
|
|
28
28
|
openModal,
|
|
29
|
+
openDeleteDataRowModal,
|
|
30
|
+
openDeleteEditorRowModal,
|
|
29
31
|
openEditConnectionModal,
|
|
30
32
|
refreshCurrentRoute,
|
|
31
33
|
removeConnection,
|
|
@@ -35,10 +37,13 @@ import {
|
|
|
35
37
|
selectStructureEntry,
|
|
36
38
|
setDataPage,
|
|
37
39
|
setDataPageSize,
|
|
40
|
+
setDataSearchColumn,
|
|
41
|
+
setDataSearchQuery,
|
|
38
42
|
setCurrentQuery,
|
|
39
43
|
setEditorTab,
|
|
40
44
|
setRoute,
|
|
41
45
|
submitCreateConnection,
|
|
46
|
+
submitDeleteRowConfirmation,
|
|
42
47
|
submitDataRowUpdate,
|
|
43
48
|
submitEditorRowUpdate,
|
|
44
49
|
submitEditConnection,
|
|
@@ -112,6 +117,35 @@ function syncQueryEditorScroll(textarea) {
|
|
|
112
117
|
highlightNode.style.transform = `translate(${-textarea.scrollLeft}px, ${-textarea.scrollTop}px)`;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
function readFileAsBase64(file) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const reader = new FileReader();
|
|
123
|
+
|
|
124
|
+
reader.onload = () => {
|
|
125
|
+
const result = String(reader.result ?? "");
|
|
126
|
+
const markerIndex = result.indexOf(",");
|
|
127
|
+
resolve(markerIndex >= 0 ? result.slice(markerIndex + 1) : result);
|
|
128
|
+
};
|
|
129
|
+
reader.onerror = () => {
|
|
130
|
+
reject(reader.error ?? new Error("The selected logo could not be read."));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
reader.readAsDataURL(file);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function buildConnectionLogoUpload(file) {
|
|
138
|
+
if (!(file instanceof File) || !file.size) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
fileName: file.name,
|
|
144
|
+
mimeType: file.type,
|
|
145
|
+
base64: await readFileAsBase64(file),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
function renderNotFoundView() {
|
|
116
150
|
return {
|
|
117
151
|
main: `
|
|
@@ -221,7 +255,9 @@ function restoreFocusedInputState(snapshot) {
|
|
|
221
255
|
function renderApp(state) {
|
|
222
256
|
const focusedInput = captureFocusedInputState();
|
|
223
257
|
const { main, panel } = resolveView(state);
|
|
224
|
-
const isLockedRoute = ["editor", "editorResults", "data"].includes(
|
|
258
|
+
const isLockedRoute = ["editor", "editorResults", "data", "structure"].includes(
|
|
259
|
+
state.route.name
|
|
260
|
+
);
|
|
225
261
|
|
|
226
262
|
teardownStructureGraph();
|
|
227
263
|
shellRefs.topNav.innerHTML = renderTopNav(state);
|
|
@@ -246,6 +282,11 @@ const router = createRouter((route) => {
|
|
|
246
282
|
setRoute(route);
|
|
247
283
|
});
|
|
248
284
|
|
|
285
|
+
async function executeEditorQueryAndNavigate() {
|
|
286
|
+
const success = await executeCurrentQuery();
|
|
287
|
+
router.navigate(success ? "/editor/results" : "/editor");
|
|
288
|
+
}
|
|
289
|
+
|
|
249
290
|
async function handleAction(actionNode) {
|
|
250
291
|
const { action } = actionNode.dataset;
|
|
251
292
|
|
|
@@ -298,10 +339,15 @@ async function handleAction(actionNode) {
|
|
|
298
339
|
await createActiveConnectionBackup();
|
|
299
340
|
return;
|
|
300
341
|
case "execute-query": {
|
|
301
|
-
|
|
302
|
-
router.navigate(success ? "/editor/results" : "/editor");
|
|
342
|
+
await executeEditorQueryAndNavigate();
|
|
303
343
|
return;
|
|
304
344
|
}
|
|
345
|
+
case "delete-data-row":
|
|
346
|
+
openDeleteDataRowModal(actionNode.dataset.rowIndex);
|
|
347
|
+
return;
|
|
348
|
+
case "delete-editor-row":
|
|
349
|
+
openDeleteEditorRowModal(actionNode.dataset.rowIndex);
|
|
350
|
+
return;
|
|
305
351
|
case "clear-query":
|
|
306
352
|
clearCurrentQuery();
|
|
307
353
|
return;
|
|
@@ -376,6 +422,27 @@ document.addEventListener("click", (event) => {
|
|
|
376
422
|
});
|
|
377
423
|
|
|
378
424
|
document.addEventListener("keydown", (event) => {
|
|
425
|
+
const target = event.target;
|
|
426
|
+
|
|
427
|
+
if (
|
|
428
|
+
event.key === "Enter" &&
|
|
429
|
+
event.shiftKey &&
|
|
430
|
+
!event.altKey &&
|
|
431
|
+
!event.ctrlKey &&
|
|
432
|
+
!event.metaKey &&
|
|
433
|
+
!event.defaultPrevented &&
|
|
434
|
+
target instanceof HTMLTextAreaElement &&
|
|
435
|
+
target.dataset.bind === "current-query"
|
|
436
|
+
) {
|
|
437
|
+
event.preventDefault();
|
|
438
|
+
|
|
439
|
+
if (!getState().editor.executing) {
|
|
440
|
+
void executeEditorQueryAndNavigate();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
379
446
|
if (event.key !== "Escape" || event.defaultPrevented) {
|
|
380
447
|
return;
|
|
381
448
|
}
|
|
@@ -411,6 +478,11 @@ document.addEventListener("input", (event) => {
|
|
|
411
478
|
syncQueryEditorHighlight(bindNode);
|
|
412
479
|
syncQueryEditorScroll(bindNode);
|
|
413
480
|
setCurrentQuery(bindNode.value);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (bindNode.dataset.bind === "data-search-query") {
|
|
485
|
+
setDataSearchQuery(bindNode.value);
|
|
414
486
|
}
|
|
415
487
|
});
|
|
416
488
|
|
|
@@ -438,6 +510,11 @@ document.addEventListener("change", (event) => {
|
|
|
438
510
|
if (bindNode.dataset.bind === "history-entry" && bindNode.value) {
|
|
439
511
|
loadQueryFromHistory(bindNode.value);
|
|
440
512
|
bindNode.value = "";
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (bindNode.dataset.bind === "data-search-column") {
|
|
517
|
+
setDataSearchColumn(bindNode.value);
|
|
441
518
|
}
|
|
442
519
|
});
|
|
443
520
|
|
|
@@ -503,6 +580,8 @@ document.addEventListener("submit", async (event) => {
|
|
|
503
580
|
case "edit-connection": {
|
|
504
581
|
const connectionId = String(formData.get("connectionId") ?? "");
|
|
505
582
|
const isActiveConnection = getState().connections.active?.id === connectionId;
|
|
583
|
+
const logoFile = formData.get("logoFile");
|
|
584
|
+
const logoUpload = await buildConnectionLogoUpload(logoFile);
|
|
506
585
|
|
|
507
586
|
if (isActiveConnection) {
|
|
508
587
|
resetStructureGraphForDatabaseChange();
|
|
@@ -512,10 +591,15 @@ document.addEventListener("submit", async (event) => {
|
|
|
512
591
|
path: String(formData.get("path") ?? ""),
|
|
513
592
|
label: String(formData.get("label") ?? ""),
|
|
514
593
|
readOnly: formData.get("readOnly") === "on",
|
|
594
|
+
clearLogo: formData.get("clearLogo") === "on" && !logoUpload,
|
|
595
|
+
logoUpload,
|
|
515
596
|
});
|
|
516
597
|
|
|
517
598
|
return;
|
|
518
599
|
}
|
|
600
|
+
case "delete-row-confirm":
|
|
601
|
+
await submitDeleteRowConfirmation();
|
|
602
|
+
return;
|
|
519
603
|
case "save-data-row": {
|
|
520
604
|
const values = {};
|
|
521
605
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
truncateMiddle,
|
|
6
6
|
} from "../utils/format.js";
|
|
7
7
|
import { renderStatusBadge } from "./badges.js";
|
|
8
|
+
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
8
9
|
|
|
9
10
|
export function renderConnectionCard(connection, activeConnectionId) {
|
|
10
11
|
const isActive = activeConnectionId
|
|
@@ -20,16 +21,14 @@ export function renderConnectionCard(connection, activeConnectionId) {
|
|
|
20
21
|
>
|
|
21
22
|
<div class="flex-1 p-6">
|
|
22
23
|
<div class="mb-6 flex items-start justify-between">
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
${renderConnectionLogo(connection, {
|
|
25
|
+
containerClass: `clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors ${
|
|
25
26
|
isActive ? "bg-primary-container" : "bg-surface-container-highest"
|
|
26
|
-
}
|
|
27
|
-
style="--clip-path: ${clipPath};"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}">database</span>
|
|
32
|
-
</div>
|
|
27
|
+
}`,
|
|
28
|
+
containerAttributes: `style="--clip-path: ${clipPath};"`,
|
|
29
|
+
imageClassName: "h-full w-full object-cover",
|
|
30
|
+
iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
|
|
31
|
+
})}
|
|
33
32
|
<div class="flex items-center gap-2">
|
|
34
33
|
${renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted")}
|
|
35
34
|
${connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : ""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderConnectionLogo(
|
|
4
|
+
connection,
|
|
5
|
+
{
|
|
6
|
+
containerClass = "",
|
|
7
|
+
containerAttributes = "",
|
|
8
|
+
imageClassName = "h-full w-full object-cover",
|
|
9
|
+
iconClassName = "",
|
|
10
|
+
icon = "database",
|
|
11
|
+
} = {}
|
|
12
|
+
) {
|
|
13
|
+
const logoUrl = connection?.logoUrl ?? null;
|
|
14
|
+
const label = connection?.label ?? "Database";
|
|
15
|
+
|
|
16
|
+
if (logoUrl) {
|
|
17
|
+
return `
|
|
18
|
+
<div ${containerAttributes} class="${containerClass}">
|
|
19
|
+
<img
|
|
20
|
+
alt="${escapeHtml(`${label} logo`)}"
|
|
21
|
+
class="${imageClassName}"
|
|
22
|
+
src="${escapeHtml(logoUrl)}"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `
|
|
29
|
+
<div ${containerAttributes} class="${containerClass}">
|
|
30
|
+
<span class="material-symbols-outlined ${iconClassName}">${escapeHtml(icon)}</span>
|
|
31
|
+
</div>
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
formatDateTime,
|
|
5
5
|
truncateMiddle,
|
|
6
6
|
} from "../utils/format.js";
|
|
7
|
+
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
7
8
|
|
|
8
9
|
function renderRecentConnections(recentConnections = []) {
|
|
9
10
|
if (!recentConnections.length) {
|
|
@@ -26,7 +27,12 @@ function renderRecentConnections(recentConnections = []) {
|
|
|
26
27
|
data-connection-id="${escapeHtml(connection.id)}"
|
|
27
28
|
type="button"
|
|
28
29
|
>
|
|
29
|
-
|
|
30
|
+
${renderConnectionLogo(connection, {
|
|
31
|
+
containerClass:
|
|
32
|
+
"flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
|
|
33
|
+
imageClassName: "h-full w-full object-cover",
|
|
34
|
+
iconClassName: "text-sm text-primary-container",
|
|
35
|
+
})}
|
|
30
36
|
<span class="min-w-0">
|
|
31
37
|
<span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
|
|
32
38
|
<span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
|
|
@@ -53,16 +59,24 @@ function renderActiveConnection(activeConnection) {
|
|
|
53
59
|
return `
|
|
54
60
|
<div class="mx-auto max-w-2xl border border-outline-variant/15 bg-surface-container-low px-6 py-5 text-left">
|
|
55
61
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
56
|
-
<div>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
<div class="flex items-start gap-4">
|
|
63
|
+
${renderConnectionLogo(activeConnection, {
|
|
64
|
+
containerClass:
|
|
65
|
+
"flex h-14 w-14 items-center justify-center overflow-hidden border border-outline-variant/15 bg-surface-container-highest",
|
|
66
|
+
imageClassName: "h-full w-full object-cover",
|
|
67
|
+
iconClassName: "text-2xl text-primary-container",
|
|
68
|
+
})}
|
|
69
|
+
<div>
|
|
70
|
+
<p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
|
|
71
|
+
ACTIVE_DATABASE
|
|
72
|
+
</p>
|
|
73
|
+
<h2 class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
74
|
+
${escapeHtml(activeConnection.label)}
|
|
75
|
+
</h2>
|
|
76
|
+
<p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
|
|
77
|
+
truncateMiddle(activeConnection.path, 72)
|
|
78
|
+
)}</p>
|
|
79
|
+
</div>
|
|
66
80
|
</div>
|
|
67
81
|
<div class="text-right text-xs text-on-surface-variant/65">
|
|
68
82
|
<div>${escapeHtml(formatBytes(activeConnection.sizeBytes))}</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml, truncateMiddle } from "../utils/format.js";
|
|
2
|
+
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
2
3
|
|
|
3
4
|
function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
|
|
4
5
|
return `
|
|
@@ -31,6 +32,32 @@ function renderCheckboxField({ label, name, checked = false, text }) {
|
|
|
31
32
|
`;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function renderFileField({
|
|
36
|
+
label,
|
|
37
|
+
name,
|
|
38
|
+
accept = "",
|
|
39
|
+
helpText = "",
|
|
40
|
+
}) {
|
|
41
|
+
return `
|
|
42
|
+
<label class="block space-y-2">
|
|
43
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
44
|
+
${escapeHtml(label)}
|
|
45
|
+
</span>
|
|
46
|
+
<input
|
|
47
|
+
accept="${escapeHtml(accept)}"
|
|
48
|
+
class="block w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface file:mr-4 file:border-0 file:bg-primary-container file:px-3 file:py-2 file:text-xs file:font-bold file:text-on-primary"
|
|
49
|
+
name="${escapeHtml(name)}"
|
|
50
|
+
type="file"
|
|
51
|
+
/>
|
|
52
|
+
${
|
|
53
|
+
helpText
|
|
54
|
+
? `<p class="text-[11px] leading-5 text-on-surface-variant/60">${escapeHtml(helpText)}</p>`
|
|
55
|
+
: ""
|
|
56
|
+
}
|
|
57
|
+
</label>
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
function renderError(error) {
|
|
35
62
|
if (!error) {
|
|
36
63
|
return "";
|
|
@@ -102,6 +129,36 @@ function renderEditConnectionForm(modal) {
|
|
|
102
129
|
placeholder: "Optional display name",
|
|
103
130
|
value: connection.label ?? "",
|
|
104
131
|
})}
|
|
132
|
+
<div class="space-y-3">
|
|
133
|
+
<span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
134
|
+
Database Icon
|
|
135
|
+
</span>
|
|
136
|
+
<div class="flex flex-wrap items-center gap-4 border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
|
|
137
|
+
${renderConnectionLogo(connection, {
|
|
138
|
+
containerClass:
|
|
139
|
+
"flex h-16 w-16 items-center justify-center overflow-hidden border border-outline-variant/20 bg-surface-container-highest",
|
|
140
|
+
imageClassName: "h-full w-full object-cover",
|
|
141
|
+
iconClassName: "text-2xl text-primary-container",
|
|
142
|
+
})}
|
|
143
|
+
<div class="min-w-0 flex-1">
|
|
144
|
+
${renderFileField({
|
|
145
|
+
label: "Upload image",
|
|
146
|
+
name: "logoFile",
|
|
147
|
+
accept: ".png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp",
|
|
148
|
+
helpText: "Allowed formats: PNG, JPG, WEBP. The file is stored in db_logos.",
|
|
149
|
+
})}
|
|
150
|
+
${
|
|
151
|
+
connection.logoUrl
|
|
152
|
+
? renderCheckboxField({
|
|
153
|
+
label: "Reset icon",
|
|
154
|
+
name: "clearLogo",
|
|
155
|
+
text: "Use the default icon again",
|
|
156
|
+
})
|
|
157
|
+
: ""
|
|
158
|
+
}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
105
162
|
${renderCheckboxField({
|
|
106
163
|
label: "Open read-only",
|
|
107
164
|
name: "readOnly",
|
|
@@ -254,6 +311,75 @@ function renderImportSqlForm(modal, state) {
|
|
|
254
311
|
`;
|
|
255
312
|
}
|
|
256
313
|
|
|
314
|
+
function renderDeleteRowConfirmForm(modal) {
|
|
315
|
+
const rowPreview = modal.rowPreview ?? [];
|
|
316
|
+
|
|
317
|
+
return `
|
|
318
|
+
<form class="space-y-5" data-form="delete-row-confirm">
|
|
319
|
+
<div class="space-y-3">
|
|
320
|
+
<p class="text-sm leading-7 text-on-surface">
|
|
321
|
+
Delete this row from <span class="font-bold text-primary-container">${escapeHtml(
|
|
322
|
+
modal.tableName ?? "the current table"
|
|
323
|
+
)}</span>?
|
|
324
|
+
</p>
|
|
325
|
+
<p class="text-sm leading-7 text-on-surface-variant/65">
|
|
326
|
+
This action cannot be undone.
|
|
327
|
+
</p>
|
|
328
|
+
${
|
|
329
|
+
modal.rowLabel
|
|
330
|
+
? `
|
|
331
|
+
<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">
|
|
332
|
+
${escapeHtml(modal.rowLabel)}
|
|
333
|
+
</div>
|
|
334
|
+
`
|
|
335
|
+
: ""
|
|
336
|
+
}
|
|
337
|
+
${
|
|
338
|
+
rowPreview.length
|
|
339
|
+
? `
|
|
340
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
341
|
+
${rowPreview
|
|
342
|
+
.map(
|
|
343
|
+
(field) => `
|
|
344
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
345
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
346
|
+
${escapeHtml(field.label)}
|
|
347
|
+
</div>
|
|
348
|
+
<div
|
|
349
|
+
class="mt-2 text-sm text-on-surface"
|
|
350
|
+
title="${escapeHtml(field.fullValue ?? field.value ?? "")}"
|
|
351
|
+
>
|
|
352
|
+
${escapeHtml(field.value ?? "")}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
`
|
|
356
|
+
)
|
|
357
|
+
.join("")}
|
|
358
|
+
</div>
|
|
359
|
+
`
|
|
360
|
+
: ""
|
|
361
|
+
}
|
|
362
|
+
</div>
|
|
363
|
+
${renderError(modal.error)}
|
|
364
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
365
|
+
<button
|
|
366
|
+
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
|
|
367
|
+
data-action="close-modal"
|
|
368
|
+
type="button"
|
|
369
|
+
>
|
|
370
|
+
Cancel
|
|
371
|
+
</button>
|
|
372
|
+
<button
|
|
373
|
+
class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
|
|
374
|
+
type="submit"
|
|
375
|
+
>
|
|
376
|
+
${modal.submitting ? "Deleting..." : "Delete Row"}
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
</form>
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
257
383
|
export function renderModal(state) {
|
|
258
384
|
const modal = state.modal;
|
|
259
385
|
|
|
@@ -282,6 +408,11 @@ export function renderModal(state) {
|
|
|
282
408
|
title: "Edit Connection",
|
|
283
409
|
body: renderEditConnectionForm(modal),
|
|
284
410
|
},
|
|
411
|
+
"delete-row": {
|
|
412
|
+
eyebrow: "Mutation // Confirm row deletion",
|
|
413
|
+
title: "Delete Row",
|
|
414
|
+
body: renderDeleteRowConfirmForm(modal),
|
|
415
|
+
},
|
|
285
416
|
};
|
|
286
417
|
|
|
287
418
|
const config = contentByKind[modal.kind];
|
|
@@ -23,16 +23,23 @@ export function renderRowEditorPanel({
|
|
|
23
23
|
disabledMessage = "",
|
|
24
24
|
saveError = null,
|
|
25
25
|
saving = false,
|
|
26
|
+
deleting = false,
|
|
26
27
|
reloadAction = "",
|
|
27
|
-
submitLabel = "Save
|
|
28
|
+
submitLabel = "Save",
|
|
29
|
+
deleteAction = "",
|
|
30
|
+
deleteRowIndex = null,
|
|
31
|
+
deleteLabel = "Delete Row",
|
|
32
|
+
deleteEnabled = false,
|
|
28
33
|
emptyEditableMessage = "This row has no editable scalar columns.",
|
|
29
34
|
}) {
|
|
30
35
|
const canSubmit = !disabledMessage && editableFields.length > 0;
|
|
36
|
+
const canDelete = !disabledMessage && deleteEnabled;
|
|
37
|
+
const formId = `${formName}-panel-form`;
|
|
31
38
|
|
|
32
39
|
return `
|
|
33
40
|
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
34
41
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
35
|
-
<div class="
|
|
42
|
+
<div class="space-y-4">
|
|
36
43
|
<div>
|
|
37
44
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
38
45
|
${escapeHtml(sectionLabel)}
|
|
@@ -50,13 +57,58 @@ export function renderRowEditorPanel({
|
|
|
50
57
|
: ""
|
|
51
58
|
}
|
|
52
59
|
</div>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
61
|
+
${
|
|
62
|
+
reloadAction
|
|
63
|
+
? `
|
|
64
|
+
<button
|
|
65
|
+
class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
|
|
66
|
+
data-action="${escapeHtml(reloadAction)}"
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
Reload
|
|
70
|
+
</button>
|
|
71
|
+
`
|
|
72
|
+
: ""
|
|
73
|
+
}
|
|
74
|
+
${
|
|
75
|
+
canSubmit
|
|
76
|
+
? `
|
|
77
|
+
<button
|
|
78
|
+
class="bg-primary-container px-5 py-3 text-[10px] font-black uppercase tracking-[0.16em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
79
|
+
form="${escapeHtml(formId)}"
|
|
80
|
+
type="submit"
|
|
81
|
+
${saving || deleting ? "disabled" : ""}
|
|
82
|
+
>
|
|
83
|
+
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
84
|
+
</button>
|
|
85
|
+
`
|
|
86
|
+
: ""
|
|
87
|
+
}
|
|
88
|
+
${
|
|
89
|
+
canDelete
|
|
90
|
+
? `
|
|
91
|
+
<button
|
|
92
|
+
class="border border-error/25 bg-error-container/10 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-colors hover:bg-error-container/20 disabled:cursor-default disabled:opacity-40"
|
|
93
|
+
data-action="${escapeHtml(deleteAction)}"
|
|
94
|
+
data-row-index="${escapeHtml(String(deleteRowIndex ?? ""))}"
|
|
95
|
+
type="button"
|
|
96
|
+
${saving || deleting ? "disabled" : ""}
|
|
97
|
+
>
|
|
98
|
+
${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
|
|
99
|
+
</button>
|
|
100
|
+
`
|
|
101
|
+
: ""
|
|
102
|
+
}
|
|
103
|
+
<button
|
|
104
|
+
aria-label="Close panel"
|
|
105
|
+
class="flex h-11 w-11 items-center justify-center border border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
|
|
106
|
+
data-action="${escapeHtml(closeAction)}"
|
|
107
|
+
type="button"
|
|
108
|
+
>
|
|
109
|
+
<span class="material-symbols-outlined text-base">close</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
60
112
|
</div>
|
|
61
113
|
</header>
|
|
62
114
|
<div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
|
|
@@ -68,7 +120,7 @@ export function renderRowEditorPanel({
|
|
|
68
120
|
</div>
|
|
69
121
|
`
|
|
70
122
|
: `
|
|
71
|
-
<form class="space-y-6" data-form="${escapeHtml(formName)}">
|
|
123
|
+
<form class="space-y-6" data-form="${escapeHtml(formName)}" id="${escapeHtml(formId)}">
|
|
72
124
|
${hiddenFields
|
|
73
125
|
.map(
|
|
74
126
|
(field) => `
|
|
@@ -92,7 +144,7 @@ export function renderRowEditorPanel({
|
|
|
92
144
|
${escapeHtml(field.label ?? field.name)}
|
|
93
145
|
</span>
|
|
94
146
|
<textarea
|
|
95
|
-
class="min-h-[
|
|
147
|
+
class="min-h-[56px] 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"
|
|
96
148
|
name="field:${escapeHtml(field.name)}"
|
|
97
149
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
98
150
|
</label>
|
|
@@ -117,28 +169,6 @@ export function renderRowEditorPanel({
|
|
|
117
169
|
`
|
|
118
170
|
: ""
|
|
119
171
|
}
|
|
120
|
-
<div class="flex items-center justify-end gap-3 border-t border-outline-variant/10 pt-6">
|
|
121
|
-
${
|
|
122
|
-
reloadAction
|
|
123
|
-
? `
|
|
124
|
-
<button
|
|
125
|
-
class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface hover:bg-surface-container-highest"
|
|
126
|
-
data-action="${escapeHtml(reloadAction)}"
|
|
127
|
-
type="button"
|
|
128
|
-
>
|
|
129
|
-
Reload
|
|
130
|
-
</button>
|
|
131
|
-
`
|
|
132
|
-
: ""
|
|
133
|
-
}
|
|
134
|
-
<button
|
|
135
|
-
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary disabled:cursor-default disabled:opacity-40"
|
|
136
|
-
type="submit"
|
|
137
|
-
${canSubmit ? "" : "disabled"}
|
|
138
|
-
>
|
|
139
|
-
${escapeHtml(saving ? "Saving..." : submitLabel)}
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
172
|
</form>
|
|
143
173
|
`
|
|
144
174
|
}
|