sqlite-hub 0.11.1 → 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 +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- 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 +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- 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 +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -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/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -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
|
@@ -9,31 +9,93 @@ function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
|
9
9
|
return sortDirection === "desc" ? "south" : "north";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const COPY_COLUMN_ACTIONS = [
|
|
13
|
+
{ mode: "column", label: "Copy column" },
|
|
14
|
+
{ mode: "column-with-header", label: "Copy column with header" },
|
|
15
|
+
{ mode: "first-10", label: "Copy first 10" },
|
|
16
|
+
{ mode: "markdown-todo", label: "Export as Markdown Todo" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function renderColumnActionMenu(columnName, resultScope) {
|
|
20
|
+
return `
|
|
21
|
+
<details class="query-result-column-menu" data-copy-column-menu>
|
|
22
|
+
<summary
|
|
23
|
+
aria-label="Column actions for ${escapeHtml(columnName)}"
|
|
24
|
+
class="query-result-column-menu__toggle"
|
|
25
|
+
title="Column actions"
|
|
26
|
+
>
|
|
27
|
+
<span class="material-symbols-outlined" aria-hidden="true">more_vert</span>
|
|
28
|
+
</summary>
|
|
29
|
+
<div class="query-result-column-menu__panel" role="menu">
|
|
30
|
+
${COPY_COLUMN_ACTIONS.map(
|
|
31
|
+
(item) => `
|
|
32
|
+
<button
|
|
33
|
+
class="query-result-column-menu__item"
|
|
34
|
+
data-action="open-copy-column-modal"
|
|
35
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
36
|
+
data-copy-mode="${escapeHtml(item.mode)}"
|
|
37
|
+
data-result-scope="${escapeHtml(resultScope)}"
|
|
38
|
+
role="menuitem"
|
|
39
|
+
type="button"
|
|
40
|
+
>
|
|
41
|
+
${escapeHtml(item.label)}
|
|
42
|
+
</button>
|
|
43
|
+
`
|
|
44
|
+
).join("")}
|
|
45
|
+
</div>
|
|
46
|
+
</details>
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }) {
|
|
13
51
|
const isActive = columnName === sortColumn;
|
|
52
|
+
const labelMarkup = `<span class="query-result-column-label truncate" title="${escapeHtml(columnName)}">${escapeHtml(
|
|
53
|
+
columnName
|
|
54
|
+
)}</span>`;
|
|
14
55
|
|
|
15
56
|
return `
|
|
16
|
-
<
|
|
17
|
-
class="
|
|
18
|
-
isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
|
|
19
|
-
}"
|
|
20
|
-
data-action="sort-editor-results-column"
|
|
57
|
+
<div
|
|
58
|
+
class="query-result-column-header"
|
|
21
59
|
data-column-name="${escapeHtml(columnName)}"
|
|
22
|
-
|
|
60
|
+
data-result-column-header
|
|
61
|
+
data-result-scope="${escapeHtml(resultScope)}"
|
|
23
62
|
>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
${
|
|
64
|
+
sortAction
|
|
65
|
+
? `
|
|
66
|
+
<button
|
|
67
|
+
class="query-result-column-sort ${
|
|
68
|
+
isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
|
|
69
|
+
}"
|
|
70
|
+
data-action="${escapeHtml(sortAction)}"
|
|
71
|
+
data-column-name="${escapeHtml(columnName)}"
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
${labelMarkup}
|
|
75
|
+
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
76
|
+
columnName,
|
|
77
|
+
sortColumn,
|
|
78
|
+
sortDirection
|
|
79
|
+
)}</span>
|
|
80
|
+
</button>
|
|
81
|
+
`
|
|
82
|
+
: `<span class="query-result-column-static text-on-surface-variant">${labelMarkup}</span>`
|
|
83
|
+
}
|
|
84
|
+
${renderColumnActionMenu(columnName, resultScope)}
|
|
85
|
+
</div>
|
|
31
86
|
`;
|
|
32
87
|
}
|
|
33
88
|
|
|
34
89
|
export function renderQueryResultsPane(
|
|
35
90
|
result,
|
|
36
|
-
{
|
|
91
|
+
{
|
|
92
|
+
selectedRowIndex = null,
|
|
93
|
+
editable = false,
|
|
94
|
+
sortColumn = null,
|
|
95
|
+
sortDirection = null,
|
|
96
|
+
resultScope = "editor",
|
|
97
|
+
sortAction = "sort-editor-results-column",
|
|
98
|
+
} = {}
|
|
37
99
|
) {
|
|
38
100
|
if (!result) {
|
|
39
101
|
return `
|
|
@@ -48,8 +110,8 @@ export function renderQueryResultsPane(
|
|
|
48
110
|
|
|
49
111
|
const columns = (result.columns ?? []).map((columnName) => ({
|
|
50
112
|
headerClassName:
|
|
51
|
-
"border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
|
|
52
|
-
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
|
|
113
|
+
"query-result-header-cell border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
|
|
114
|
+
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }),
|
|
53
115
|
cellClassName: "px-4 py-3 align-top text-on-surface",
|
|
54
116
|
render: (row) => {
|
|
55
117
|
const value = formatCellValue(row[columnName]);
|
|
@@ -1,4 +1,19 @@
|
|
|
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";
|
|
15
|
+
|
|
16
|
+
const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
2
17
|
|
|
3
18
|
function getJsonPreview(value) {
|
|
4
19
|
if (value === null || value === undefined) {
|
|
@@ -28,6 +43,241 @@ function getJsonPreview(value) {
|
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
function getUrlValue(value) {
|
|
47
|
+
const text = String(value ?? "").trim();
|
|
48
|
+
|
|
49
|
+
if (!URL_PATTERN.test(text)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(text);
|
|
55
|
+
|
|
56
|
+
return ["http:", "https:"].includes(url.protocol) ? url.href : null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function withUrlBadge(badges = [], url) {
|
|
63
|
+
if (!url) {
|
|
64
|
+
return badges;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasUrlBadge = badges.some((badge) => {
|
|
68
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
69
|
+
return String(label ?? "").toUpperCase() === "URL";
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getAllowedValues(field) {
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
|
|
78
|
+
return (Array.isArray(field.allowedValues) ? field.allowedValues : [])
|
|
79
|
+
.map((value) => String(value))
|
|
80
|
+
.filter((value) => {
|
|
81
|
+
if (seen.has(value)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
seen.add(value);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function withCheckBadge(badges = [], allowedValues = []) {
|
|
91
|
+
if (!allowedValues.length) {
|
|
92
|
+
return badges;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hasCheckBadge = badges.some((badge) => {
|
|
96
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
97
|
+
return String(label ?? "").toUpperCase() === "CHECK";
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return hasCheckBadge ? badges : [...badges, { label: "CHECK", tone: "check" }];
|
|
101
|
+
}
|
|
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
|
+
|
|
116
|
+
function renderOpenUrlButton(url) {
|
|
117
|
+
if (!url) {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `
|
|
122
|
+
<div class="mt-2">
|
|
123
|
+
<button
|
|
124
|
+
class="standard-button"
|
|
125
|
+
data-action="open-row-editor-url"
|
|
126
|
+
data-url="${escapeHtml(url)}"
|
|
127
|
+
type="button"
|
|
128
|
+
>
|
|
129
|
+
<span class="material-symbols-outlined text-sm">open_in_new</span>
|
|
130
|
+
Open in tab
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderAllowedValuesSelect(field, allowedValues) {
|
|
137
|
+
const currentValue = String(field.value ?? "");
|
|
138
|
+
const hasCurrentAllowedValue = allowedValues.includes(currentValue);
|
|
139
|
+
const shouldRenderEmptyOption = field.notNull !== true;
|
|
140
|
+
const shouldRenderCurrentOption =
|
|
141
|
+
currentValue !== "" && !hasCurrentAllowedValue;
|
|
142
|
+
const options = [
|
|
143
|
+
shouldRenderEmptyOption
|
|
144
|
+
? `<option value="" ${currentValue === "" ? "selected" : ""}>NULL / empty</option>`
|
|
145
|
+
: "",
|
|
146
|
+
shouldRenderCurrentOption
|
|
147
|
+
? `<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue)}</option>`
|
|
148
|
+
: "",
|
|
149
|
+
...allowedValues.map(
|
|
150
|
+
(value) =>
|
|
151
|
+
`<option value="${escapeHtml(value)}" ${
|
|
152
|
+
value === currentValue ? "selected" : ""
|
|
153
|
+
}>${escapeHtml(value)}</option>`
|
|
154
|
+
),
|
|
155
|
+
].join("");
|
|
156
|
+
|
|
157
|
+
return `
|
|
158
|
+
<select
|
|
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
|
|
163
|
+
name="field:${escapeHtml(field.name)}"
|
|
164
|
+
>
|
|
165
|
+
${options}
|
|
166
|
+
</select>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
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
|
+
|
|
31
281
|
function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
32
282
|
return `
|
|
33
283
|
<div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
|
|
@@ -41,37 +291,66 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
41
291
|
`;
|
|
42
292
|
}
|
|
43
293
|
|
|
44
|
-
function renderReadonlyField(
|
|
45
|
-
const
|
|
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);
|
|
301
|
+
const url = getUrlValue(value);
|
|
302
|
+
const badges = withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview);
|
|
46
303
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
47
304
|
const jsonPreview = getJsonPreview(value);
|
|
48
305
|
|
|
49
306
|
return `
|
|
50
|
-
<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
|
+
${
|
|
313
|
+
url ? "data-row-editor-url-field" : ""
|
|
314
|
+
}>
|
|
51
315
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
52
316
|
<span>${escapeHtml(displayLabel)}</span>
|
|
53
|
-
${badges
|
|
317
|
+
${renderFieldBadgesWithCharacterCount(badges, rawValue)}
|
|
54
318
|
</div>
|
|
55
319
|
${
|
|
56
320
|
jsonPreview
|
|
57
321
|
? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
|
|
58
322
|
: `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
|
|
59
323
|
}
|
|
324
|
+
${renderTimestampPreview({ ...field, rawValue }, tableMeta)}
|
|
325
|
+
${renderFilePathPreview({ ...field, rawValue }, tableMeta)}
|
|
326
|
+
${renderOpenUrlButton(url)}
|
|
60
327
|
</div>
|
|
61
328
|
`;
|
|
62
329
|
}
|
|
63
330
|
|
|
64
|
-
function renderEditableField(field) {
|
|
65
|
-
const
|
|
331
|
+
function renderEditableField(field, tableMeta = {}) {
|
|
332
|
+
const columnName = getFieldColumnName(field);
|
|
333
|
+
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
334
|
+
const url = getUrlValue(field.value);
|
|
335
|
+
const allowedValues = getAllowedValues(field);
|
|
336
|
+
const filePathPreview = getFieldFilePathPreview(field, tableMeta);
|
|
337
|
+
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
338
|
+
const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
|
|
66
339
|
const jsonPreview = getJsonPreview(field.value);
|
|
67
340
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
68
341
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
69
342
|
|
|
70
343
|
return `
|
|
71
|
-
<
|
|
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
|
+
>
|
|
72
351
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
73
352
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
74
|
-
${badges.
|
|
353
|
+
${renderFieldBadgesWithCharacterCount(badges, inputType === "number" ? null : field.value ?? "")}
|
|
75
354
|
</span>
|
|
76
355
|
${
|
|
77
356
|
jsonPreview
|
|
@@ -79,27 +358,51 @@ function renderEditableField(field) {
|
|
|
79
358
|
: ""
|
|
80
359
|
}
|
|
81
360
|
${
|
|
82
|
-
|
|
361
|
+
allowedValues.length && !jsonPreview
|
|
362
|
+
? renderAllowedValuesSelect(field, allowedValues)
|
|
363
|
+
: inputType === "number" && !jsonPreview
|
|
83
364
|
? `
|
|
84
365
|
<input
|
|
85
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
|
|
86
369
|
name="field:${escapeHtml(field.name)}"
|
|
87
370
|
step="${escapeHtml(numberStep)}"
|
|
88
371
|
type="number"
|
|
89
372
|
value="${escapeHtml(field.value ?? "")}"
|
|
90
373
|
/>
|
|
91
374
|
`
|
|
375
|
+
: url && !jsonPreview
|
|
376
|
+
? `
|
|
377
|
+
<input
|
|
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
|
|
382
|
+
name="field:${escapeHtml(field.name)}"
|
|
383
|
+
data-row-editor-url-input
|
|
384
|
+
spellcheck="false"
|
|
385
|
+
type="text"
|
|
386
|
+
value="${escapeHtml(field.value ?? "")}"
|
|
387
|
+
/>
|
|
388
|
+
${renderOpenUrlButton(url)}
|
|
389
|
+
`
|
|
92
390
|
: `
|
|
93
391
|
<textarea
|
|
94
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 ${
|
|
95
393
|
jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
|
|
96
394
|
}"
|
|
395
|
+
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
396
|
+
data-row-editor-text-source
|
|
397
|
+
data-row-editor-timestamp-source
|
|
97
398
|
name="field:${escapeHtml(field.name)}"
|
|
98
399
|
spellcheck="false"
|
|
99
400
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
100
401
|
`
|
|
101
402
|
}
|
|
102
|
-
|
|
403
|
+
${renderTimestampPreview(field, tableMeta)}
|
|
404
|
+
${renderFilePathPreview(field, tableMeta)}
|
|
405
|
+
</div>
|
|
103
406
|
`;
|
|
104
407
|
}
|
|
105
408
|
|
|
@@ -112,6 +415,22 @@ function getFieldBadgeClassName(tone) {
|
|
|
112
415
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
113
416
|
}
|
|
114
417
|
|
|
418
|
+
if (tone === "url") {
|
|
419
|
+
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (tone === "check") {
|
|
423
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
424
|
+
}
|
|
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
|
+
|
|
115
434
|
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
116
435
|
}
|
|
117
436
|
|
|
@@ -135,11 +454,13 @@ export function renderRowEditorPanel({
|
|
|
135
454
|
hiddenFields = [],
|
|
136
455
|
editableFields = [],
|
|
137
456
|
readonlyFields = [],
|
|
457
|
+
tableMeta = {},
|
|
138
458
|
disabledMessage = "",
|
|
139
459
|
saveError = null,
|
|
140
460
|
saving = false,
|
|
141
461
|
deleting = false,
|
|
142
462
|
reloadAction = "",
|
|
463
|
+
jsonActionsEnabled = false,
|
|
143
464
|
submitLabel = "Save",
|
|
144
465
|
deleteAction = "",
|
|
145
466
|
deleteRowIndex = null,
|
|
@@ -158,6 +479,18 @@ export function renderRowEditorPanel({
|
|
|
158
479
|
'" type="button">Reload</button>',
|
|
159
480
|
].join("")
|
|
160
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
|
+
: "",
|
|
161
494
|
canSubmit
|
|
162
495
|
? [
|
|
163
496
|
'<button class="standard-button" form="',
|
|
@@ -254,7 +587,7 @@ export function renderRowEditorPanel({
|
|
|
254
587
|
? `
|
|
255
588
|
<div class="space-y-4">
|
|
256
589
|
${editableFields
|
|
257
|
-
.map((field) => renderEditableField(field))
|
|
590
|
+
.map((field) => renderEditableField(field, tableMeta))
|
|
258
591
|
.join("")}
|
|
259
592
|
</div>
|
|
260
593
|
`
|
|
@@ -286,7 +619,7 @@ export function renderRowEditorPanel({
|
|
|
286
619
|
</div>
|
|
287
620
|
<div class="space-y-3">
|
|
288
621
|
${readonlyFields
|
|
289
|
-
.map((field) => renderReadonlyField(field
|
|
622
|
+
.map((field) => renderReadonlyField(field, tableMeta))
|
|
290
623
|
.join("")}
|
|
291
624
|
</div>
|
|
292
625
|
</div>
|