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.
Files changed (53) hide show
  1. package/README.md +106 -19
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +34 -0
  5. package/frontend/js/app.js +481 -32
  6. package/frontend/js/components/modal.js +270 -50
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryHistoryPanel.js +126 -131
  9. package/frontend/js/components/queryResults.js +18 -1
  10. package/frontend/js/components/rowEditorPanel.js +42 -34
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +8 -0
  13. package/frontend/js/store.js +655 -2
  14. package/frontend/js/utils/jsonPreview.js +31 -0
  15. package/frontend/js/utils/markdownDocuments.js +248 -0
  16. package/frontend/js/utils/rowEditorValues.js +41 -0
  17. package/frontend/js/utils/tableScrollState.js +39 -0
  18. package/frontend/js/views/charts.js +8 -0
  19. package/frontend/js/views/data.js +4 -1
  20. package/frontend/js/views/documents.js +300 -0
  21. package/frontend/js/views/editor.js +2 -0
  22. package/frontend/js/views/settings.js +39 -3
  23. package/frontend/styles/components.css +19 -0
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +422 -0
  26. package/package.json +2 -1
  27. package/server/middleware/localRequestSecurity.js +84 -0
  28. package/server/routes/connections.js +18 -1
  29. package/server/routes/documents.js +163 -0
  30. package/server/routes/settings.js +22 -6
  31. package/server/server.js +18 -1
  32. package/server/services/nativeFileDialogService.js +168 -0
  33. package/server/services/sqlite/dataBrowserService.js +0 -4
  34. package/server/services/sqlite/exportService.js +5 -1
  35. package/server/services/sqlite/sqlExecutor.js +51 -2
  36. package/server/services/storage/appStateStore.js +313 -0
  37. package/server/utils/sqliteTypes.js +17 -8
  38. package/tests/cli-args.test.js +100 -0
  39. package/tests/connections-file-dialog-route.test.js +46 -0
  40. package/tests/copy-column-modal.test.js +97 -0
  41. package/tests/database-documents.test.js +85 -0
  42. package/tests/export-blob.test.js +46 -0
  43. package/tests/json-preview.test.js +49 -0
  44. package/tests/local-request-security.test.js +85 -0
  45. package/tests/markdown-documents.test.js +79 -0
  46. package/tests/native-file-dialog.test.js +78 -0
  47. package/tests/query-results-truncation.test.js +20 -0
  48. package/tests/row-editor-null-values.test.js +131 -0
  49. package/tests/settings-metadata.test.js +16 -0
  50. package/tests/settings-view.test.js +32 -0
  51. package/tests/sql-identifier-safety.test.js +9 -3
  52. package/tests/sql-result-limit.test.js +66 -0
  53. package/tests/table-scroll-state.test.js +70 -0
@@ -1,133 +1,130 @@
1
- import {
2
- escapeHtml,
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
- if (queryType === "select" || queryType === "update") {
9
- return "success";
10
- }
5
+ if (queryType === 'select' || queryType === 'update') {
6
+ return 'success';
7
+ }
11
8
 
12
- if (queryType === "pragma") {
13
- return "primary";
14
- }
9
+ if (queryType === 'pragma') {
10
+ return 'primary';
11
+ }
15
12
 
16
- return "muted";
13
+ return 'muted';
17
14
  }
18
15
 
19
16
  export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId) {
20
- const isActive = Number(activeHistoryId) === Number(item.id);
21
- const isSelected = Number(selectedHistoryId) === Number(item.id);
22
- const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
23
- const itemClasses = [
24
- "query-history-item",
25
- isActive ? "is-active" : "",
26
- item.lastRun?.status === "error" ? "is-error" : "",
27
- ]
28
- .filter(Boolean)
29
- .join(" ");
30
- const tableMarkup = visibleTables
31
- .map((tableName) =>
32
- [
33
- '<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">',
34
- escapeHtml(tableName),
35
- "</span>",
36
- ].join("")
37
- )
38
- .join("");
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
- return [
41
- '<article class="',
42
- itemClasses,
43
- '"><button class="query-history-item-hit ',
44
- isActive ? "is-active" : "",
45
- '" data-action="select-query-history-item" data-history-id="',
46
- escapeHtml(item.id),
47
- '" type="button">',
48
- '<div class="flex flex-wrap items-center gap-2"><span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">',
49
- escapeHtml(item.displayTitle),
50
- "</span>",
51
- renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType)),
52
- item.isSaved ? renderStatusBadge("saved", "primary") : "",
53
- item.isDestructive ? renderStatusBadge("destructive", "warning") : "",
54
- "</div>",
55
- '<p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">',
56
- escapeHtml(item.previewSql),
57
- "</p></button>",
58
- '<div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">',
59
- '<div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">',
60
- tableMarkup,
61
- '</div><div class="flex items-center gap-1">',
62
- '<button class="query-history-icon-button" data-action="open-query-history" data-history-id="',
63
- escapeHtml(item.id),
64
- '" title="Open in editor" type="button"><span class="material-symbols-outlined text-[18px]">edit_note</span></button>',
65
- '<button class="query-history-icon-button" data-action="run-query-history" data-history-id="',
66
- escapeHtml(item.id),
67
- '" title="Run query" type="button"><span class="material-symbols-outlined text-[18px]">play_arrow</span></button>',
68
- '<button class="query-history-icon-button ',
69
- item.isSaved ? "is-active" : "",
70
- '" data-action="toggle-query-history-saved" data-history-id="',
71
- escapeHtml(item.id),
72
- '" data-next-value="',
73
- item.isSaved ? "false" : "true",
74
- '" title="',
75
- item.isSaved ? "Remove from saved" : "Save query",
76
- '" type="button"><span class="material-symbols-outlined text-[18px]">',
77
- item.isSaved ? "bookmark" : "bookmark_add",
78
- "</span></button>",
79
- '<button class="query-history-icon-button ',
80
- isSelected ? "is-active" : "",
81
- '" data-action="select-query-history-item" data-history-id="',
82
- escapeHtml(item.id),
83
- '" title="Open query detail" type="button"><span class="material-symbols-outlined text-[18px]">info</span></button>',
84
- "</div></div></article>",
85
- ].join("");
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
- const tabs = [
90
- { id: "recent", label: "Recent" },
91
- { id: "saved", label: "Saved" },
92
- { id: "unsaved", label: "Unsaved" },
93
- { id: "failed", label: "Failed" },
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
- return [
97
- '<div class="flex items-center gap-2">',
98
- tabs
99
- .map((tab) =>
100
- [
101
- '<button class="query-history-tab ',
102
- activeTab === tab.id ? "is-active" : "",
103
- '" data-action="set-query-history-tab" data-tab="',
104
- tab.id,
105
- '" type="button">',
106
- escapeHtml(tab.label),
107
- "</button>",
108
- ].join("")
109
- )
110
- .join(""),
111
- '<span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">',
112
- escapeHtml(formatNumber(historyTotal)),
113
- "</span></div>",
114
- ].join("");
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
- items = [],
119
- loading = false,
120
- loadingMore = false,
121
- error = null,
122
- activeTab = "recent",
123
- search = "",
124
- committedSearch = "",
125
- total = 0,
126
- hasMore = false,
127
- activeHistoryId = null,
128
- selectedHistoryId = null,
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
- return `
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 ? "true" : "false"}"
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
- error
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
- !loading && !items.length
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
- loading
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
- hasMore
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 ? "Loading..." : "Load More"}
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
- <div class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest">
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" : ""}>NULL / empty</option>`
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 whitespace-pre-wrap break-words border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface">${escapeHtml(
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 = getJsonPreview(value);
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 = getJsonPreview(field.value);
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',
@@ -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: {} };