sqlite-hub 0.5.0 → 0.6.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 +2 -2
- package/changelog.md +8 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
- package/frontend/js/api.js +15 -0
- package/frontend/js/app.js +279 -8
- package/frontend/js/components/queryEditor.js +2 -2
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/tableDesignerEditor.js +356 -0
- package/frontend/js/components/tableDesignerSidebar.js +126 -0
- package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +296 -1
- package/frontend/js/utils/tableDesigner.js +1192 -0
- package/frontend/js/views/data.js +253 -263
- package/frontend/js/views/tableDesigner.js +37 -0
- package/frontend/styles/base.css +87 -73
- package/frontend/styles/components.css +798 -251
- package/frontend/styles/views.css +40 -0
- package/package.json +1 -1
- package/server/routes/tableDesigner.js +60 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
- package/server/services/sqlite/tableDesigner/sql.js +63 -0
- package/server/services/sqlite/tableDesigner/validation.js +245 -0
- package/server/services/sqlite/tableDesignerService.js +181 -0
- package/frontend/assets/mockups/data_edit.png +0 -0
- package/frontend/assets/mockups/graph_visualize.png +0 -0
- package/frontend/assets/mockups/overview.png +0 -0
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import { renderDataGrid } from
|
|
2
|
-
import { renderRowEditorPanel } from
|
|
3
|
-
import {
|
|
4
|
-
escapeHtml,
|
|
5
|
-
formatCellValue,
|
|
6
|
-
formatNumber,
|
|
7
|
-
isBlobPreview,
|
|
8
|
-
truncateMiddle,
|
|
9
|
-
} from "../utils/format.js";
|
|
1
|
+
import { renderDataGrid } from '../components/dataGrid.js';
|
|
2
|
+
import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
|
|
3
|
+
import { escapeHtml, formatCellValue, formatNumber, isBlobPreview, truncateMiddle } from '../utils/format.js';
|
|
10
4
|
|
|
11
5
|
function getSelectedRow(state) {
|
|
12
|
-
|
|
6
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
if (typeof rowIndex !== 'number') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
17
11
|
|
|
18
|
-
|
|
12
|
+
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
function renderTableList(state) {
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const tables = state.dataBrowser.tables ?? [];
|
|
17
|
+
const activeName = state.dataBrowser.selectedTable;
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
if (state.dataBrowser.loading && !tables.length) {
|
|
20
|
+
return `
|
|
27
21
|
<div class="flex flex-1 items-center justify-center px-6">
|
|
28
22
|
<div class="text-center text-on-surface-variant/40">
|
|
29
23
|
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
@@ -31,82 +25,82 @@ function renderTableList(state) {
|
|
|
31
25
|
</div>
|
|
32
26
|
</div>
|
|
33
27
|
`;
|
|
34
|
-
|
|
28
|
+
}
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
if (!tables.length) {
|
|
31
|
+
return `
|
|
38
32
|
<div class="px-6 py-6 text-sm text-on-surface-variant/55">
|
|
39
33
|
No tables found in the active SQLite database.
|
|
40
34
|
</div>
|
|
41
35
|
`;
|
|
42
|
-
|
|
36
|
+
}
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
return `
|
|
45
39
|
<div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
|
|
46
40
|
<div class="space-y-2">
|
|
47
41
|
${tables
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
.map(
|
|
43
|
+
table => `
|
|
50
44
|
<button
|
|
51
45
|
class="w-full border px-4 py-3 text-left transition-colors ${
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
table.name === activeName
|
|
47
|
+
? 'border-primary-container/30 bg-surface-container-high'
|
|
48
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
55
49
|
}"
|
|
56
50
|
data-action="navigate"
|
|
57
51
|
data-to="/data/${encodeURIComponent(table.name)}"
|
|
58
52
|
type="button"
|
|
59
53
|
>
|
|
60
54
|
<div class="truncate font-mono text-xs ${
|
|
61
|
-
|
|
55
|
+
table.name === activeName ? 'text-primary-container' : 'text-on-surface'
|
|
62
56
|
}">
|
|
63
57
|
${escapeHtml(table.name)}
|
|
64
58
|
</div>
|
|
65
59
|
</button>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
`,
|
|
61
|
+
)
|
|
62
|
+
.join('')}
|
|
69
63
|
</div>
|
|
70
64
|
</div>
|
|
71
65
|
`;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
function renderWorkspaceHeader(state) {
|
|
75
|
-
|
|
69
|
+
const table = state.dataBrowser.table;
|
|
76
70
|
|
|
77
|
-
|
|
71
|
+
return `
|
|
78
72
|
<header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
|
|
79
73
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
|
80
|
-
<div>
|
|
74
|
+
<div class="data-headline-container">
|
|
81
75
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
82
76
|
Data Browser
|
|
83
77
|
</div>
|
|
84
78
|
<h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
85
|
-
${escapeHtml(table?.name ??
|
|
79
|
+
${escapeHtml(table?.name ?? 'Table Data')}
|
|
86
80
|
</h1>
|
|
87
81
|
<div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
88
82
|
${
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
table
|
|
84
|
+
? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
|
|
85
|
+
formatNumber(table.columns?.length ?? 0),
|
|
86
|
+
)}`
|
|
87
|
+
: `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
|
|
94
88
|
}
|
|
95
89
|
</div>
|
|
96
90
|
</div>
|
|
97
91
|
<div class="flex items-center gap-3">
|
|
98
92
|
${
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
table
|
|
94
|
+
? `
|
|
101
95
|
<button
|
|
102
96
|
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"
|
|
103
97
|
data-action="export-data-csv"
|
|
104
98
|
type="button"
|
|
105
99
|
>
|
|
106
|
-
${state.dataBrowser.exportLoading ?
|
|
100
|
+
${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export CSV'}
|
|
107
101
|
</button>
|
|
108
102
|
`
|
|
109
|
-
|
|
103
|
+
: ''
|
|
110
104
|
}
|
|
111
105
|
<button
|
|
112
106
|
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"
|
|
@@ -116,8 +110,8 @@ function renderWorkspaceHeader(state) {
|
|
|
116
110
|
Reload Data
|
|
117
111
|
</button>
|
|
118
112
|
${
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
table
|
|
114
|
+
? `
|
|
121
115
|
<button
|
|
122
116
|
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"
|
|
123
117
|
data-action="navigate"
|
|
@@ -127,7 +121,7 @@ function renderWorkspaceHeader(state) {
|
|
|
127
121
|
Open Structure
|
|
128
122
|
</button>
|
|
129
123
|
`
|
|
130
|
-
|
|
124
|
+
: ''
|
|
131
125
|
}
|
|
132
126
|
</div>
|
|
133
127
|
</div>
|
|
@@ -136,11 +130,11 @@ function renderWorkspaceHeader(state) {
|
|
|
136
130
|
}
|
|
137
131
|
|
|
138
132
|
function renderWorkspaceError(state) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
if (!state.dataBrowser.error) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
142
136
|
|
|
143
|
-
|
|
137
|
+
return `
|
|
144
138
|
<div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
|
|
145
139
|
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
146
140
|
${escapeHtml(state.dataBrowser.error.code)}
|
|
@@ -151,38 +145,38 @@ function renderWorkspaceError(state) {
|
|
|
151
145
|
}
|
|
152
146
|
|
|
153
147
|
function getCellWidthClass(columnName) {
|
|
154
|
-
|
|
148
|
+
const normalized = String(columnName ?? '').toLowerCase();
|
|
155
149
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
|
|
151
|
+
return 'max-w-[18rem]';
|
|
152
|
+
}
|
|
159
153
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
|
|
155
|
+
return 'max-w-[11rem]';
|
|
156
|
+
}
|
|
163
157
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
if (/(id|uuid|token|key)/.test(normalized)) {
|
|
159
|
+
return 'max-w-[10rem]';
|
|
160
|
+
}
|
|
167
161
|
|
|
168
|
-
|
|
162
|
+
return 'max-w-[12rem]';
|
|
169
163
|
}
|
|
170
164
|
|
|
171
165
|
function getSortIcon(columnName, sortColumn, sortDirection) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
if (columnName !== sortColumn) {
|
|
167
|
+
return 'unfold_more';
|
|
168
|
+
}
|
|
175
169
|
|
|
176
|
-
|
|
170
|
+
return sortDirection === 'desc' ? 'south' : 'north';
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
180
|
-
|
|
174
|
+
const isActive = columnName === sortColumn;
|
|
181
175
|
|
|
182
|
-
|
|
176
|
+
return `
|
|
183
177
|
<button
|
|
184
178
|
class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
|
|
185
|
-
|
|
179
|
+
isActive ? 'text-primary-container' : 'text-on-surface-variant hover:text-primary-container'
|
|
186
180
|
}"
|
|
187
181
|
data-action="${action}"
|
|
188
182
|
data-column-name="${escapeHtml(columnName)}"
|
|
@@ -190,52 +184,54 @@ function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
|
|
|
190
184
|
>
|
|
191
185
|
<span class="truncate">${escapeHtml(columnName)}</span>
|
|
192
186
|
<span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
columnName,
|
|
188
|
+
sortColumn,
|
|
189
|
+
sortDirection,
|
|
196
190
|
)}</span>
|
|
197
191
|
</button>
|
|
198
192
|
`;
|
|
199
193
|
}
|
|
200
194
|
|
|
201
195
|
function getFilteredTableRows(table, state) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
index
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
const allRows = table?.rows ?? [];
|
|
197
|
+
const availableColumns = table?.columns ?? [];
|
|
198
|
+
const searchQuery = String(state.dataBrowser.searchQuery ?? '')
|
|
199
|
+
.trim()
|
|
200
|
+
.toLowerCase();
|
|
201
|
+
const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
|
|
202
|
+
? state.dataBrowser.searchColumn
|
|
203
|
+
: (availableColumns[0] ?? '');
|
|
204
|
+
|
|
205
|
+
const indexedRows = allRows.map((row, index) => ({
|
|
206
|
+
row,
|
|
207
|
+
index,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
if (!searchQuery || !activeColumn) {
|
|
211
|
+
return {
|
|
212
|
+
activeColumn,
|
|
213
|
+
filteredRows: indexedRows,
|
|
214
|
+
searchQuery,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
215
218
|
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
activeColumn,
|
|
220
|
+
searchQuery,
|
|
221
|
+
filteredRows: indexedRows.filter(({ row }) =>
|
|
222
|
+
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery),
|
|
223
|
+
),
|
|
219
224
|
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
activeColumn,
|
|
224
|
-
searchQuery,
|
|
225
|
-
filteredRows: indexedRows.filter(({ row }) =>
|
|
226
|
-
formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
|
|
227
|
-
),
|
|
228
|
-
};
|
|
229
225
|
}
|
|
230
226
|
|
|
231
227
|
function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
232
|
-
|
|
228
|
+
const columns = table?.columns ?? [];
|
|
233
229
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
if (!table || !columns.length) {
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
237
233
|
|
|
238
|
-
|
|
234
|
+
return `
|
|
239
235
|
<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
|
|
240
236
|
<label class="flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-4 py-3">
|
|
241
237
|
<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
|
|
@@ -244,7 +240,7 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
|
244
240
|
data-bind="data-search-query"
|
|
245
241
|
placeholder="Filter current page..."
|
|
246
242
|
type="search"
|
|
247
|
-
value="${escapeHtml(state.dataBrowser.searchQuery ??
|
|
243
|
+
value="${escapeHtml(state.dataBrowser.searchQuery ?? '')}"
|
|
248
244
|
/>
|
|
249
245
|
</label>
|
|
250
246
|
<select
|
|
@@ -252,31 +248,27 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
|
|
|
252
248
|
data-bind="data-search-column"
|
|
253
249
|
>
|
|
254
250
|
${columns
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
<option value="${escapeHtml(columnName)}" ${
|
|
258
|
-
columnName === activeColumn ? "selected" : ""
|
|
259
|
-
}>
|
|
251
|
+
.map(
|
|
252
|
+
columnName => `
|
|
253
|
+
<option value="${escapeHtml(columnName)}" ${columnName === activeColumn ? 'selected' : ''}>
|
|
260
254
|
${escapeHtml(columnName)}
|
|
261
255
|
</option>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
`,
|
|
257
|
+
)
|
|
258
|
+
.join('')}
|
|
265
259
|
</select>
|
|
266
260
|
<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
|
|
267
|
-
${escapeHtml(formatNumber(filteredRowCount))} match${
|
|
268
|
-
filteredRowCount === 1 ? "" : "es"
|
|
269
|
-
} on this page
|
|
261
|
+
${escapeHtml(formatNumber(filteredRowCount))} match${filteredRowCount === 1 ? '' : 'es'} on this page
|
|
270
262
|
</div>
|
|
271
263
|
</div>
|
|
272
264
|
`;
|
|
273
265
|
}
|
|
274
266
|
|
|
275
267
|
function renderTableSurface(state) {
|
|
276
|
-
|
|
268
|
+
const table = state.dataBrowser.table;
|
|
277
269
|
|
|
278
|
-
|
|
279
|
-
|
|
270
|
+
if (state.dataBrowser.tableLoading && !table) {
|
|
271
|
+
return `
|
|
280
272
|
<div class="flex flex-1 items-center justify-center">
|
|
281
273
|
<div class="text-center text-on-surface-variant/40">
|
|
282
274
|
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
@@ -284,10 +276,10 @@ function renderTableSurface(state) {
|
|
|
284
276
|
</div>
|
|
285
277
|
</div>
|
|
286
278
|
`;
|
|
287
|
-
|
|
279
|
+
}
|
|
288
280
|
|
|
289
|
-
|
|
290
|
-
|
|
281
|
+
if (!table) {
|
|
282
|
+
return `
|
|
291
283
|
<div class="flex flex-1 items-center justify-center px-8 text-center">
|
|
292
284
|
<div>
|
|
293
285
|
<div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
|
|
@@ -299,89 +291,88 @@ function renderTableSurface(state) {
|
|
|
299
291
|
</div>
|
|
300
292
|
</div>
|
|
301
293
|
`;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return `
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
|
|
297
|
+
const sortColumn = state.dataBrowser.sortColumn;
|
|
298
|
+
const sortDirection = state.dataBrowser.sortDirection;
|
|
299
|
+
const columns = (table.columns ?? []).map(columnName => ({
|
|
300
|
+
headerClassName:
|
|
301
|
+
'border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container',
|
|
302
|
+
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, 'sort-data-column'),
|
|
303
|
+
cellClassName: 'px-4 py-2 align-top text-[11px] text-on-surface',
|
|
304
|
+
render: row => {
|
|
305
|
+
const value = formatCellValue(row[columnName]);
|
|
306
|
+
const isNull = value === 'NULL';
|
|
307
|
+
const widthClass = getCellWidthClass(columnName);
|
|
308
|
+
const displayValue = isNull ? value : truncateMiddle(value, 48);
|
|
309
|
+
|
|
310
|
+
return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
|
|
311
|
+
isNull ? 'text-on-surface-variant/45' : 'text-on-surface'
|
|
312
|
+
}" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
|
|
313
|
+
},
|
|
314
|
+
}));
|
|
315
|
+
const totalRows = table.rowCount ?? 0;
|
|
316
|
+
const page = table.page ?? state.dataBrowser.page ?? 1;
|
|
317
|
+
const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
|
|
318
|
+
const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
|
|
319
|
+
const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
|
|
320
|
+
const pageSizes = [25, 50, 100];
|
|
321
|
+
const filteredRowCount = filteredRows.length;
|
|
322
|
+
const hasActiveSearch = Boolean(searchQuery);
|
|
323
|
+
|
|
324
|
+
return `
|
|
334
325
|
<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
|
|
335
326
|
${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
|
|
336
327
|
<div class="custom-scrollbar flex-1 overflow-auto">
|
|
337
328
|
${renderDataGrid({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
329
|
+
columns,
|
|
330
|
+
rows: filteredRows.map(({ row }) => row),
|
|
331
|
+
tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
|
|
332
|
+
theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
|
|
333
|
+
tbodyClass: 'divide-y divide-outline-variant/5',
|
|
334
|
+
getRowClass: (_, filteredIndex) => {
|
|
335
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
336
|
+
|
|
337
|
+
return `${
|
|
338
|
+
state.dataBrowser.selectedRowIndex === rowIndex
|
|
339
|
+
? 'bg-surface-bright'
|
|
340
|
+
: filteredIndex % 2 === 0
|
|
341
|
+
? 'bg-surface-container-low'
|
|
342
|
+
: 'bg-surface-container-lowest'
|
|
343
|
+
} cursor-pointer transition-colors hover:bg-surface-container-high`;
|
|
344
|
+
},
|
|
345
|
+
getRowAttrs: (_, filteredIndex) => {
|
|
346
|
+
const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
|
|
347
|
+
|
|
348
|
+
return `data-action="select-data-row" data-row-index="${rowIndex}"`;
|
|
349
|
+
},
|
|
359
350
|
})}
|
|
360
351
|
${
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
!table.rows?.length
|
|
353
|
+
? `
|
|
363
354
|
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
364
355
|
<p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
|
|
365
356
|
TABLE_IS_EMPTY
|
|
366
357
|
</p>
|
|
367
358
|
</div>
|
|
368
359
|
`
|
|
369
|
-
|
|
370
|
-
|
|
360
|
+
: !filteredRowCount
|
|
361
|
+
? `
|
|
371
362
|
<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
|
|
372
363
|
<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
|
|
373
|
-
${hasActiveSearch ?
|
|
364
|
+
${hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.'}
|
|
374
365
|
</p>
|
|
375
366
|
</div>
|
|
376
367
|
`
|
|
377
|
-
|
|
368
|
+
: ''
|
|
378
369
|
}
|
|
379
370
|
</div>
|
|
380
371
|
<footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
|
|
381
372
|
<div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
382
373
|
showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
|
|
383
|
-
|
|
384
|
-
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` :
|
|
374
|
+
formatNumber(totalRows),
|
|
375
|
+
)} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ''}
|
|
385
376
|
</div>
|
|
386
377
|
<div class="flex flex-wrap items-center gap-4">
|
|
387
378
|
<div class="flex items-center gap-2">
|
|
@@ -390,13 +381,13 @@ function renderTableSurface(state) {
|
|
|
390
381
|
</span>
|
|
391
382
|
<div class="flex items-center gap-2">
|
|
392
383
|
${pageSizes
|
|
393
|
-
|
|
394
|
-
|
|
384
|
+
.map(
|
|
385
|
+
pageSize => `
|
|
395
386
|
<button
|
|
396
387
|
class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
388
|
+
pageSize === (table.limit ?? state.dataBrowser.pageSize)
|
|
389
|
+
? 'border-primary-container/30 bg-surface-container-high text-primary-container'
|
|
390
|
+
: 'border-outline-variant/20 text-on-surface hover:bg-surface-container-highest'
|
|
400
391
|
}"
|
|
401
392
|
data-action="set-data-page-size"
|
|
402
393
|
data-page-size="${pageSize}"
|
|
@@ -404,9 +395,9 @@ function renderTableSurface(state) {
|
|
|
404
395
|
>
|
|
405
396
|
${pageSize}
|
|
406
397
|
</button>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
398
|
+
`,
|
|
399
|
+
)
|
|
400
|
+
.join('')}
|
|
410
401
|
</div>
|
|
411
402
|
</div>
|
|
412
403
|
<div class="flex items-center gap-2">
|
|
@@ -415,7 +406,7 @@ function renderTableSurface(state) {
|
|
|
415
406
|
data-action="set-data-page"
|
|
416
407
|
data-page="${page - 1}"
|
|
417
408
|
type="button"
|
|
418
|
-
${page <= 1 ?
|
|
409
|
+
${page <= 1 ? 'disabled' : ''}
|
|
419
410
|
>
|
|
420
411
|
Prev
|
|
421
412
|
</button>
|
|
@@ -427,7 +418,7 @@ function renderTableSurface(state) {
|
|
|
427
418
|
data-action="set-data-page"
|
|
428
419
|
data-page="${page + 1}"
|
|
429
420
|
type="button"
|
|
430
|
-
${page >= pageCount ?
|
|
421
|
+
${page >= pageCount ? 'disabled' : ''}
|
|
431
422
|
>
|
|
432
423
|
Next
|
|
433
424
|
</button>
|
|
@@ -439,84 +430,83 @@ function renderTableSurface(state) {
|
|
|
439
430
|
}
|
|
440
431
|
|
|
441
432
|
function renderDataRowEditorPanel(state) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (!table || !row || typeof rowIndex !== "number") {
|
|
447
|
-
return "";
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const identityColumns =
|
|
451
|
-
table.identityStrategy?.type === "primaryKey" ? table.identityStrategy.columns ?? [] : [];
|
|
452
|
-
const editableColumns = (table.columnMeta ?? []).filter((column) => {
|
|
453
|
-
if (!column.visible || column.generated) {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
433
|
+
const table = state.dataBrowser.table;
|
|
434
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
435
|
+
const row = getSelectedRow(state);
|
|
456
436
|
|
|
457
|
-
if (
|
|
458
|
-
|
|
437
|
+
if (!table || !row || typeof rowIndex !== 'number') {
|
|
438
|
+
return '';
|
|
459
439
|
}
|
|
460
440
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
441
|
+
const identityColumns = table.identityStrategy?.type === 'primaryKey' ? (table.identityStrategy.columns ?? []) : [];
|
|
442
|
+
const editableColumns = (table.columnMeta ?? []).filter(column => {
|
|
443
|
+
if (!column.visible || column.generated) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
465
446
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (!column.visible) {
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
447
|
+
if (identityColumns.includes(column.name)) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
472
450
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
451
|
+
const value = row[column.name];
|
|
452
|
+
if (isBlobPreview(value) || (value && typeof value === 'object')) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
const readonlyColumns = (table.columnMeta ?? []).filter(column => {
|
|
459
|
+
if (!column.visible) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (identityColumns.includes(column.name) || column.generated) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
476
466
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
467
|
+
const value = row[column.name];
|
|
468
|
+
return isBlobPreview(value) || (value && typeof value === 'object');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
return renderRowEditorPanel({
|
|
472
|
+
title: table.name,
|
|
473
|
+
sectionLabel: 'Row Editor',
|
|
474
|
+
subtitle: `row ${rowIndex + 1}`,
|
|
475
|
+
closeAction: 'clear-data-row-selection',
|
|
476
|
+
formName: 'save-data-row',
|
|
477
|
+
hiddenFields: [{ name: 'rowIndex', value: String(rowIndex) }],
|
|
478
|
+
disabledMessage: state.connections.active?.readOnly
|
|
479
|
+
? 'The active database is opened read-only, so row editing is disabled.'
|
|
480
|
+
: table.notSafelyUpdatable
|
|
481
|
+
? 'This table has no stable identity column, so SQLite Hub cannot safely update rows.'
|
|
482
|
+
: '',
|
|
483
|
+
editableFields: editableColumns.map(column => {
|
|
484
|
+
const value = row[column.name];
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
name: column.name,
|
|
488
|
+
label: column.name,
|
|
489
|
+
value: value === null || value === undefined ? '' : String(value),
|
|
490
|
+
};
|
|
491
|
+
}),
|
|
492
|
+
readonlyFields: readonlyColumns.map(column => ({
|
|
493
|
+
name: column.name,
|
|
494
|
+
label: column.name,
|
|
495
|
+
value: formatCellValue(row[column.name]),
|
|
496
|
+
})),
|
|
497
|
+
saveError: state.dataBrowser.saveError,
|
|
498
|
+
saving: state.dataBrowser.saving,
|
|
499
|
+
deleting: state.dataBrowser.deleting,
|
|
500
|
+
deleteAction: 'delete-data-row',
|
|
501
|
+
deleteRowIndex: rowIndex,
|
|
502
|
+
deleteEnabled: Boolean(row.__identity),
|
|
503
|
+
reloadAction: 'reload-data-route',
|
|
504
|
+
});
|
|
515
505
|
}
|
|
516
506
|
|
|
517
507
|
export function renderDataView(state) {
|
|
518
|
-
|
|
519
|
-
|
|
508
|
+
return {
|
|
509
|
+
main: `
|
|
520
510
|
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
|
|
521
511
|
<div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
|
|
522
512
|
<aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
|
|
@@ -538,6 +528,6 @@ export function renderDataView(state) {
|
|
|
538
528
|
</div>
|
|
539
529
|
</section>
|
|
540
530
|
`,
|
|
541
|
-
|
|
542
|
-
|
|
531
|
+
panel: renderDataRowEditorPanel(state),
|
|
532
|
+
};
|
|
543
533
|
}
|