sqlite-hub 0.16.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -19
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +481 -32
- package/frontend/js/components/modal.js +270 -50
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +18 -1
- package/frontend/js/components/rowEditorPanel.js +42 -34
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +655 -2
- package/frontend/js/utils/jsonPreview.js +31 -0
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/utils/rowEditorValues.js +41 -0
- package/frontend/js/utils/tableScrollState.js +39 -0
- package/frontend/js/views/charts.js +8 -0
- package/frontend/js/views/data.js +4 -1
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +39 -3
- package/frontend/styles/components.css +19 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +2 -1
- package/server/middleware/localRequestSecurity.js +84 -0
- package/server/routes/connections.js +18 -1
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +18 -1
- package/server/services/nativeFileDialogService.js +168 -0
- package/server/services/sqlite/dataBrowserService.js +0 -4
- package/server/services/sqlite/exportService.js +5 -1
- package/server/services/sqlite/sqlExecutor.js +51 -2
- package/server/services/storage/appStateStore.js +313 -0
- package/server/utils/sqliteTypes.js +17 -8
- package/tests/cli-args.test.js +100 -0
- package/tests/connections-file-dialog-route.test.js +46 -0
- package/tests/copy-column-modal.test.js +97 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-blob.test.js +46 -0
- package/tests/json-preview.test.js +49 -0
- package/tests/local-request-security.test.js +85 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/native-file-dialog.test.js +78 -0
- package/tests/query-results-truncation.test.js +20 -0
- package/tests/row-editor-null-values.test.js +131 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
- package/tests/sql-identifier-safety.test.js +9 -3
- package/tests/sql-result-limit.test.js +66 -0
- package/tests/table-scroll-state.test.js +70 -0
|
@@ -1,133 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
formatNumber,
|
|
4
|
-
} from "../utils/format.js";
|
|
5
|
-
import { renderStatusBadge } from "./badges.js";
|
|
1
|
+
import { escapeHtml, formatNumber } from '../utils/format.js';
|
|
2
|
+
import { renderStatusBadge } from './badges.js';
|
|
6
3
|
|
|
7
4
|
function getQueryTypeTone(queryType) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
if (queryType === 'select' || queryType === 'update') {
|
|
6
|
+
return 'success';
|
|
7
|
+
}
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
if (queryType === 'pragma') {
|
|
10
|
+
return 'primary';
|
|
11
|
+
}
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
return 'muted';
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
17
|
+
const isActive = Number(activeHistoryId) === Number(item.id);
|
|
18
|
+
const isSelected = Number(selectedHistoryId) === Number(item.id);
|
|
19
|
+
const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
|
|
20
|
+
const itemClasses = [
|
|
21
|
+
'query-history-item',
|
|
22
|
+
isActive ? 'is-active' : '',
|
|
23
|
+
item.lastRun?.status === 'error' ? 'is-error' : '',
|
|
24
|
+
]
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join(' ');
|
|
27
|
+
const tableMarkup = visibleTables
|
|
28
|
+
.map(tableName =>
|
|
29
|
+
[
|
|
30
|
+
'<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">',
|
|
31
|
+
escapeHtml(tableName),
|
|
32
|
+
'</span>',
|
|
33
|
+
].join(''),
|
|
34
|
+
)
|
|
35
|
+
.join('');
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
37
|
+
return [
|
|
38
|
+
'<article class="',
|
|
39
|
+
itemClasses,
|
|
40
|
+
'"><button class="query-history-item-hit ',
|
|
41
|
+
isActive ? 'is-active' : '',
|
|
42
|
+
'" data-action="select-query-history-item" data-history-id="',
|
|
43
|
+
escapeHtml(item.id),
|
|
44
|
+
'" type="button">',
|
|
45
|
+
'<div class="flex flex-wrap items-center gap-2"><span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface w-full">',
|
|
46
|
+
escapeHtml(item.displayTitle),
|
|
47
|
+
'</span>',
|
|
48
|
+
renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
|
|
49
|
+
item.isSaved ? renderStatusBadge('saved', 'primary') : '',
|
|
50
|
+
item.isDestructive ? renderStatusBadge('destructive', 'warning') : '',
|
|
51
|
+
'</div>',
|
|
52
|
+
'<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">',
|
|
53
|
+
escapeHtml(item.previewSql),
|
|
54
|
+
'</p></button>',
|
|
55
|
+
'<div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">',
|
|
56
|
+
'<div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">',
|
|
57
|
+
tableMarkup,
|
|
58
|
+
'</div><div class="flex items-center gap-1">',
|
|
59
|
+
'<button class="query-history-icon-button" data-action="open-query-history" data-history-id="',
|
|
60
|
+
escapeHtml(item.id),
|
|
61
|
+
'" title="Open in editor" type="button"><span class="material-symbols-outlined text-[18px]">edit_note</span></button>',
|
|
62
|
+
'<button class="query-history-icon-button" data-action="run-query-history" data-history-id="',
|
|
63
|
+
escapeHtml(item.id),
|
|
64
|
+
'" title="Run query" type="button"><span class="material-symbols-outlined text-[18px]">play_arrow</span></button>',
|
|
65
|
+
'<button class="query-history-icon-button ',
|
|
66
|
+
item.isSaved ? 'is-active' : '',
|
|
67
|
+
'" data-action="toggle-query-history-saved" data-history-id="',
|
|
68
|
+
escapeHtml(item.id),
|
|
69
|
+
'" data-next-value="',
|
|
70
|
+
item.isSaved ? 'false' : 'true',
|
|
71
|
+
'" title="',
|
|
72
|
+
item.isSaved ? 'Remove from saved' : 'Save query',
|
|
73
|
+
'" type="button"><span class="material-symbols-outlined text-[18px]">',
|
|
74
|
+
item.isSaved ? 'bookmark' : 'bookmark_add',
|
|
75
|
+
'</span></button>',
|
|
76
|
+
'<button class="query-history-icon-button ',
|
|
77
|
+
isSelected ? 'is-active' : '',
|
|
78
|
+
'" data-action="select-query-history-item" data-history-id="',
|
|
79
|
+
escapeHtml(item.id),
|
|
80
|
+
'" title="Open query detail" type="button"><span class="material-symbols-outlined text-[18px]">info</span></button>',
|
|
81
|
+
'</div></div></article>',
|
|
82
|
+
].join('');
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
function renderQueryHistoryTabs(activeTab, historyTotal) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
const tabs = [
|
|
87
|
+
{ id: 'recent', label: 'Recent' },
|
|
88
|
+
{ id: 'saved', label: 'Saved' },
|
|
89
|
+
{ id: 'unsaved', label: 'Unsaved' },
|
|
90
|
+
{ id: 'failed', label: 'Failed' },
|
|
91
|
+
];
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
return [
|
|
94
|
+
'<div class="flex items-center gap-2">',
|
|
95
|
+
tabs
|
|
96
|
+
.map(tab =>
|
|
97
|
+
[
|
|
98
|
+
'<button class="query-history-tab ',
|
|
99
|
+
activeTab === tab.id ? 'is-active' : '',
|
|
100
|
+
'" data-action="set-query-history-tab" data-tab="',
|
|
101
|
+
tab.id,
|
|
102
|
+
'" type="button">',
|
|
103
|
+
escapeHtml(tab.label),
|
|
104
|
+
'</button>',
|
|
105
|
+
].join(''),
|
|
106
|
+
)
|
|
107
|
+
.join(''),
|
|
108
|
+
'<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">',
|
|
109
|
+
escapeHtml(formatNumber(historyTotal)),
|
|
110
|
+
'</span></div>',
|
|
111
|
+
].join('');
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
export function renderQueryHistoryPanel({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
115
|
+
items = [],
|
|
116
|
+
loading = false,
|
|
117
|
+
loadingMore = false,
|
|
118
|
+
error = null,
|
|
119
|
+
activeTab = 'recent',
|
|
120
|
+
search = '',
|
|
121
|
+
committedSearch = '',
|
|
122
|
+
total = 0,
|
|
123
|
+
hasMore = false,
|
|
124
|
+
activeHistoryId = null,
|
|
125
|
+
selectedHistoryId = null,
|
|
129
126
|
}) {
|
|
130
|
-
|
|
127
|
+
return `
|
|
131
128
|
<aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
|
|
132
129
|
<div class="border-b border-outline-variant/10 px-4 py-4">
|
|
133
130
|
<div class="flex items-center justify-between gap-3">
|
|
@@ -162,23 +159,23 @@ export function renderQueryHistoryPanel({
|
|
|
162
159
|
<div
|
|
163
160
|
class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3"
|
|
164
161
|
data-query-history-committed-search="${escapeHtml(committedSearch)}"
|
|
165
|
-
data-query-history-loading-more="${loadingMore ?
|
|
162
|
+
data-query-history-loading-more="${loadingMore ? 'true' : 'false'}"
|
|
166
163
|
data-query-history-search="${escapeHtml(search)}"
|
|
167
164
|
data-query-history-scroll
|
|
168
165
|
data-query-history-tab="${escapeHtml(activeTab)}"
|
|
169
166
|
>
|
|
170
167
|
${
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
error
|
|
169
|
+
? `
|
|
173
170
|
<div class="border border-error/30 bg-error-container/20 px-4 py-3 text-sm text-error">
|
|
174
171
|
${escapeHtml(error.message)}
|
|
175
172
|
</div>
|
|
176
173
|
`
|
|
177
|
-
|
|
174
|
+
: ''
|
|
178
175
|
}
|
|
179
176
|
${
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
!loading && !items.length
|
|
178
|
+
? `
|
|
182
179
|
<div class="flex h-full min-h-[240px] flex-col items-center justify-center px-6 text-center">
|
|
183
180
|
<span class="material-symbols-outlined text-4xl text-on-surface-variant/25">manage_search</span>
|
|
184
181
|
<p class="mt-4 font-headline text-lg font-bold uppercase tracking-tight text-on-surface">
|
|
@@ -189,36 +186,34 @@ export function renderQueryHistoryPanel({
|
|
|
189
186
|
</p>
|
|
190
187
|
</div>
|
|
191
188
|
`
|
|
192
|
-
|
|
189
|
+
: ''
|
|
193
190
|
}
|
|
194
191
|
<div class="space-y-3">
|
|
195
|
-
${items
|
|
196
|
-
.map((item) => renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId))
|
|
197
|
-
.join("")}
|
|
192
|
+
${items.map(item => renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId)).join('')}
|
|
198
193
|
</div>
|
|
199
194
|
${
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
loading
|
|
196
|
+
? `
|
|
202
197
|
<div class="mt-4 text-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/50">
|
|
203
198
|
Loading query history...
|
|
204
199
|
</div>
|
|
205
200
|
`
|
|
206
|
-
|
|
201
|
+
: ''
|
|
207
202
|
}
|
|
208
203
|
${
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
hasMore
|
|
205
|
+
? `
|
|
211
206
|
<div class="mt-4 flex justify-center">
|
|
212
207
|
<button
|
|
213
208
|
class="standard-button"
|
|
214
209
|
data-action="load-more-query-history"
|
|
215
210
|
type="button"
|
|
216
211
|
>
|
|
217
|
-
${loadingMore ?
|
|
212
|
+
${loadingMore ? 'Loading...' : 'Load More'}
|
|
218
213
|
</button>
|
|
219
214
|
</div>
|
|
220
215
|
`
|
|
221
|
-
|
|
216
|
+
: ''
|
|
222
217
|
}
|
|
223
218
|
</div>
|
|
224
219
|
</aside>
|
|
@@ -95,6 +95,7 @@ export function renderQueryResultsPane(
|
|
|
95
95
|
sortDirection = null,
|
|
96
96
|
resultScope = "editor",
|
|
97
97
|
sortAction = "sort-editor-results-column",
|
|
98
|
+
scrollKey = "",
|
|
98
99
|
} = {}
|
|
99
100
|
) {
|
|
100
101
|
if (!result) {
|
|
@@ -124,7 +125,23 @@ export function renderQueryResultsPane(
|
|
|
124
125
|
|
|
125
126
|
return `
|
|
126
127
|
<div class="relative flex h-full min-h-0 flex-col overflow-hidden bg-surface-container">
|
|
127
|
-
|
|
128
|
+
${
|
|
129
|
+
result.truncated
|
|
130
|
+
? `
|
|
131
|
+
<div class="border-b border-primary-container/20 bg-primary-container/10 px-4 py-2 text-xs text-on-surface">
|
|
132
|
+
Showing the first ${escapeHtml(String(result.rowLimit ?? result.rows?.length ?? 0))} rows. Refine the query or export it to process the complete result set.
|
|
133
|
+
</div>
|
|
134
|
+
`
|
|
135
|
+
: ""
|
|
136
|
+
}
|
|
137
|
+
<div
|
|
138
|
+
class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest"
|
|
139
|
+
${
|
|
140
|
+
scrollKey
|
|
141
|
+
? `data-table-horizontal-scroll data-table-scroll-key="${escapeHtml(scrollKey)}"`
|
|
142
|
+
: ""
|
|
143
|
+
}
|
|
144
|
+
>
|
|
128
145
|
${
|
|
129
146
|
result.columns?.length
|
|
130
147
|
? renderDataGrid({
|
|
@@ -12,37 +12,14 @@ import {
|
|
|
12
12
|
formatTextCellCharacterCount,
|
|
13
13
|
getTextCellCharacterCount,
|
|
14
14
|
} from "../utils/textCellStats.js";
|
|
15
|
+
import { formatJsonPreview } from "../utils/jsonPreview.js";
|
|
16
|
+
import {
|
|
17
|
+
getRowEditorValueState,
|
|
18
|
+
getRowEditorValueStateLabel,
|
|
19
|
+
} from "../utils/rowEditorValues.js";
|
|
15
20
|
|
|
16
21
|
const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
17
22
|
|
|
18
|
-
function getJsonPreview(value) {
|
|
19
|
-
if (value === null || value === undefined) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (typeof value === "object") {
|
|
24
|
-
return JSON.stringify(value, null, 2);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const text = String(value).trim();
|
|
28
|
-
|
|
29
|
-
if (!text || !["{", "["].includes(text[0])) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const parsed = JSON.parse(text);
|
|
35
|
-
|
|
36
|
-
if (!parsed || typeof parsed !== "object") {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return JSON.stringify(parsed, null, 2);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
23
|
function getUrlValue(value) {
|
|
47
24
|
const text = String(value ?? "").trim();
|
|
48
25
|
|
|
@@ -119,7 +96,7 @@ function renderOpenUrlButton(url) {
|
|
|
119
96
|
}
|
|
120
97
|
|
|
121
98
|
return `
|
|
122
|
-
<div class="mt-2">
|
|
99
|
+
<div class="mt-2" data-row-editor-url-action>
|
|
123
100
|
<button
|
|
124
101
|
class="standard-button"
|
|
125
102
|
data-action="open-row-editor-url"
|
|
@@ -141,7 +118,7 @@ function renderAllowedValuesSelect(field, allowedValues) {
|
|
|
141
118
|
currentValue !== "" && !hasCurrentAllowedValue;
|
|
142
119
|
const options = [
|
|
143
120
|
shouldRenderEmptyOption
|
|
144
|
-
? `<option value="" ${currentValue === "" ? "selected" : ""}>
|
|
121
|
+
? `<option value="" ${currentValue === "" ? "selected" : ""}>Empty string</option>`
|
|
145
122
|
: "",
|
|
146
123
|
shouldRenderCurrentOption
|
|
147
124
|
? `<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue)}</option>`
|
|
@@ -160,6 +137,7 @@ function renderAllowedValuesSelect(field, allowedValues) {
|
|
|
160
137
|
data-row-editor-initial-value="${escapeHtml(currentValue)}"
|
|
161
138
|
data-row-editor-text-source
|
|
162
139
|
data-row-editor-timestamp-source
|
|
140
|
+
data-row-editor-value-source
|
|
163
141
|
name="field:${escapeHtml(field.name)}"
|
|
164
142
|
>
|
|
165
143
|
${options}
|
|
@@ -284,7 +262,7 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
284
262
|
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/75">
|
|
285
263
|
${escapeHtml(title)}
|
|
286
264
|
</div>
|
|
287
|
-
<pre class="custom-scrollbar mt-3 max-h-[18rem] overflow-auto
|
|
265
|
+
<pre class="row-editor-json-preview custom-scrollbar mt-3 max-h-[18rem] overflow-auto border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface">${escapeHtml(
|
|
288
266
|
prettyJson
|
|
289
267
|
)}</pre>
|
|
290
268
|
</div>
|
|
@@ -301,7 +279,8 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
301
279
|
const url = getUrlValue(value);
|
|
302
280
|
const badges = withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview);
|
|
303
281
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
304
|
-
const jsonPreview =
|
|
282
|
+
const jsonPreview = formatJsonPreview(value);
|
|
283
|
+
const valueState = getRowEditorValueState(rawValue);
|
|
305
284
|
|
|
306
285
|
return `
|
|
307
286
|
<div
|
|
@@ -315,6 +294,7 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
315
294
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
316
295
|
<span>${escapeHtml(displayLabel)}</span>
|
|
317
296
|
${renderFieldBadgesWithCharacterCount(badges, rawValue)}
|
|
297
|
+
${renderValueStateBadge(valueState)}
|
|
318
298
|
</div>
|
|
319
299
|
${
|
|
320
300
|
jsonPreview
|
|
@@ -328,6 +308,28 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
328
308
|
`;
|
|
329
309
|
}
|
|
330
310
|
|
|
311
|
+
function getValueStateBadgeClassName(state) {
|
|
312
|
+
if (state === "null") {
|
|
313
|
+
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (state === "empty") {
|
|
317
|
+
return "border-outline-variant/35 bg-surface-container-high text-on-surface-variant";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderValueStateBadge(state) {
|
|
324
|
+
return `
|
|
325
|
+
<span
|
|
326
|
+
class="border px-2 py-1 text-[9px] ${getValueStateBadgeClassName(state)}"
|
|
327
|
+
data-row-editor-value-state
|
|
328
|
+
data-value-state="${escapeHtml(state)}"
|
|
329
|
+
>${escapeHtml(getRowEditorValueStateLabel(state))}</span>
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
331
333
|
function renderEditableField(field, tableMeta = {}) {
|
|
332
334
|
const columnName = getFieldColumnName(field);
|
|
333
335
|
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
@@ -336,25 +338,28 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
336
338
|
const filePathPreview = getFieldFilePathPreview(field, tableMeta);
|
|
337
339
|
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
338
340
|
const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
|
|
339
|
-
const jsonPreview =
|
|
341
|
+
const jsonPreview = formatJsonPreview(field.value);
|
|
340
342
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
341
343
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
344
|
+
const valueState = getRowEditorValueState(getFieldRawValue(field));
|
|
342
345
|
|
|
343
346
|
return `
|
|
344
347
|
<div
|
|
345
348
|
class="block space-y-2"
|
|
346
349
|
data-row-editor-column-name="${escapeHtml(columnName)}"
|
|
347
350
|
data-row-editor-field
|
|
351
|
+
data-row-editor-initial-state="${escapeHtml(valueState)}"
|
|
348
352
|
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
349
353
|
${url ? "data-row-editor-url-field" : ""}
|
|
350
354
|
>
|
|
351
355
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
352
356
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
353
357
|
${renderFieldBadgesWithCharacterCount(badges, inputType === "number" ? null : field.value ?? "")}
|
|
358
|
+
${renderValueStateBadge(valueState)}
|
|
354
359
|
</span>
|
|
355
360
|
${
|
|
356
361
|
jsonPreview
|
|
357
|
-
? renderJsonViewer(jsonPreview, "JSON Preview")
|
|
362
|
+
? `<div data-row-editor-json-preview-container>${renderJsonViewer(jsonPreview, "JSON Preview")}</div>`
|
|
358
363
|
: ""
|
|
359
364
|
}
|
|
360
365
|
${
|
|
@@ -366,6 +371,7 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
366
371
|
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
372
|
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
368
373
|
data-row-editor-timestamp-source
|
|
374
|
+
data-row-editor-value-source
|
|
369
375
|
name="field:${escapeHtml(field.name)}"
|
|
370
376
|
step="${escapeHtml(numberStep)}"
|
|
371
377
|
type="number"
|
|
@@ -379,6 +385,7 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
379
385
|
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
380
386
|
data-row-editor-text-source
|
|
381
387
|
data-row-editor-timestamp-source
|
|
388
|
+
data-row-editor-value-source
|
|
382
389
|
name="field:${escapeHtml(field.name)}"
|
|
383
390
|
data-row-editor-url-input
|
|
384
391
|
spellcheck="false"
|
|
@@ -395,6 +402,7 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
395
402
|
data-row-editor-initial-value="${escapeHtml(field.value ?? "")}"
|
|
396
403
|
data-row-editor-text-source
|
|
397
404
|
data-row-editor-timestamp-source
|
|
405
|
+
data-row-editor-value-source
|
|
398
406
|
name="field:${escapeHtml(field.name)}"
|
|
399
407
|
spellcheck="false"
|
|
400
408
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
@@ -8,6 +8,7 @@ const sidebarItems = [
|
|
|
8
8
|
{ label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
|
|
9
9
|
{ label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
|
|
10
10
|
{ label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
|
|
11
|
+
{ label: 'Documents', href: '#/documents', key: 'documents', icon: 'description' },
|
|
11
12
|
{ label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
|
|
12
13
|
{
|
|
13
14
|
label: 'MEDIA_TAGGING',
|
package/frontend/js/router.js
CHANGED
|
@@ -21,6 +21,14 @@ export function parseHash(hash = window.location.hash) {
|
|
|
21
21
|
historyId: segments[1] ? decodeURIComponent(segments[1]) : null,
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
+
case 'documents':
|
|
25
|
+
return {
|
|
26
|
+
name: 'documents',
|
|
27
|
+
path: cleanPath,
|
|
28
|
+
params: {
|
|
29
|
+
documentId: segments[1] ? decodeURIComponent(segments[1]) : null,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
24
32
|
case 'editor':
|
|
25
33
|
if (segments[1] === 'results') {
|
|
26
34
|
return { name: 'editorResults', path: '/editor/results', params: {} };
|