sqlite-hub 2.0.1 → 2.1.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.
Files changed (85) hide show
  1. package/README.md +14 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +10 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/structure.js +28 -5
  61. package/frontend/js/views/tableAdvisor.js +14 -6
  62. package/frontend/js/views/tableDesigner.js +44 -9
  63. package/frontend/styles/components.css +676 -79
  64. package/frontend/styles/structure-graph.css +49 -0
  65. package/frontend/styles/tailwind.generated.css +1 -1
  66. package/package.json +1 -1
  67. package/server/routes/connections.js +12 -0
  68. package/server/routes/data.js +1 -1
  69. package/server/routes/tableDesigner.js +15 -1
  70. package/server/services/mcpToolService.js +28 -0
  71. package/server/services/sqlite/connectionManager.js +23 -2
  72. package/server/services/sqlite/dataBrowserService.js +21 -0
  73. package/server/services/sqlite/introspection.js +39 -1
  74. package/server/services/sqlite/structureService.js +66 -11
  75. package/server/services/sqlite/tableAdvisor.js +6 -0
  76. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  77. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  78. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  79. package/server/services/sqlite/tableDesignerService.js +110 -5
  80. package/server/services/storage/appStateStore.js +303 -4
  81. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  82. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  83. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  84. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  85. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -0,0 +1,400 @@
1
+ import { escapeHtml, formatNumber, highlightSql } from "../utils/format.js";
2
+
3
+ function normalizeColumnName(name) {
4
+ return String(name ?? "").trim().toLowerCase();
5
+ }
6
+
7
+ function quoteIdentifier(identifier) {
8
+ return `"${String(identifier ?? "").replaceAll('"', '""')}"`;
9
+ }
10
+
11
+ function normalizeCheckExpression(expression) {
12
+ const normalized = String(expression ?? "").trim();
13
+
14
+ if (!normalized) {
15
+ return "";
16
+ }
17
+
18
+ return /^CHECK\s*\(/i.test(normalized) ? normalized : `CHECK (${normalized})`;
19
+ }
20
+
21
+ function checkIncludesColumn(constraint, column) {
22
+ if (constraint.columnId && column?.id) {
23
+ return constraint.columnId === column.id;
24
+ }
25
+
26
+ const normalizedColumn = normalizeColumnName(column?.name);
27
+
28
+ if (!normalizedColumn) {
29
+ return false;
30
+ }
31
+
32
+ if (constraint.columnId) {
33
+ return true;
34
+ }
35
+
36
+ return (constraint.columns ?? []).some(
37
+ (column) => normalizeColumnName(column.name) === normalizedColumn
38
+ );
39
+ }
40
+
41
+ function getVisibleChecks(draft, column) {
42
+ const checks = (draft?.checkConstraints ?? []).filter((constraint) => !constraint.deleted);
43
+
44
+ if (!column) {
45
+ return checks;
46
+ }
47
+
48
+ return checks.filter((constraint) => {
49
+ if (!(constraint.columns ?? []).length && !constraint.columnId) {
50
+ return true;
51
+ }
52
+
53
+ return checkIncludesColumn(constraint, column);
54
+ });
55
+ }
56
+
57
+ function isTextColumn(column) {
58
+ return ["TEXT", "DATE", "DATETIME"].includes(String(column?.type ?? "").trim().toUpperCase());
59
+ }
60
+
61
+ function isNumericColumn(column) {
62
+ return ["INTEGER", "REAL", "NUMERIC"].includes(String(column?.type ?? "").trim().toUpperCase());
63
+ }
64
+
65
+ function isBooleanLikeColumn(column) {
66
+ const type = String(column?.type ?? "").trim().toUpperCase();
67
+ const name = String(column?.name ?? "").trim().toLowerCase();
68
+
69
+ return type === "BOOLEAN" || (type === "INTEGER" && /^(is_|has_|can_|in_stock$)/.test(name));
70
+ }
71
+
72
+ function renderPresetButton(id, label, activePresetId) {
73
+ return `
74
+ <button
75
+ class="standard-button table-designer-check-drawer__preset-button ${activePresetId === id ? "is-active" : ""}"
76
+ data-action="apply-table-designer-check-preset"
77
+ data-preset-id="${escapeHtml(id)}"
78
+ title="${escapeHtml(label)}"
79
+ type="button"
80
+ >
81
+ <span>${escapeHtml(label)}</span>
82
+ </button>
83
+ `;
84
+ }
85
+
86
+ function renderPresetParameters(editor) {
87
+ const presetId = editor?.presetId ?? "";
88
+ const fields = editor?.presetFields ?? {};
89
+
90
+ if (presetId === "text-min-length") {
91
+ return renderNumberField("MIN LENGTH", "minLength", fields.minLength ?? "3");
92
+ }
93
+
94
+ if (presetId === "text-max-length") {
95
+ return renderNumberField("MAX LENGTH", "maxLength", fields.maxLength ?? "255");
96
+ }
97
+
98
+ if (presetId === "text-length-range") {
99
+ return `
100
+ <div class="grid grid-cols-2 gap-3">
101
+ ${renderNumberField("MIN", "minLength", fields.minLength ?? "3")}
102
+ ${renderNumberField("MAX", "maxLength", fields.maxLength ?? "255")}
103
+ </div>
104
+ `;
105
+ }
106
+
107
+ if (presetId === "text-allowed-values") {
108
+ return `
109
+ <label class="table-designer-check-drawer__field">
110
+ <span>Allowed Values</span>
111
+ <textarea
112
+ class="table-designer-field table-designer-check-drawer__textarea custom-scrollbar"
113
+ data-bind="table-designer-check-editor-field"
114
+ data-field="allowedValues"
115
+ spellcheck="false"
116
+ >${escapeHtml(fields.allowedValues ?? "draft\npublished\narchived")}</textarea>
117
+ </label>
118
+ `;
119
+ }
120
+
121
+ if (presetId === "numeric-min-value") {
122
+ return renderNumberField("MIN VALUE", "minValue", fields.minValue ?? "0");
123
+ }
124
+
125
+ if (presetId === "numeric-max-value") {
126
+ return renderNumberField("MAX VALUE", "maxValue", fields.maxValue ?? "100");
127
+ }
128
+
129
+ if (presetId === "numeric-range") {
130
+ return `
131
+ <div class="grid grid-cols-2 gap-3">
132
+ ${renderNumberField("MIN", "minValue", fields.minValue ?? "0")}
133
+ ${renderNumberField("MAX", "maxValue", fields.maxValue ?? "100")}
134
+ </div>
135
+ `;
136
+ }
137
+
138
+ return "";
139
+ }
140
+
141
+ function renderNumberField(label, field, value) {
142
+ return `
143
+ <label class="table-designer-check-drawer__field">
144
+ <span>${escapeHtml(label)}</span>
145
+ <input
146
+ class="table-designer-field"
147
+ data-bind="table-designer-check-editor-field"
148
+ data-field="${escapeHtml(field)}"
149
+ inputmode="numeric"
150
+ spellcheck="false"
151
+ type="text"
152
+ value="${escapeHtml(value)}"
153
+ />
154
+ </label>
155
+ `;
156
+ }
157
+
158
+ function renderPresetButtons(column, activePresetId) {
159
+ if (!column) {
160
+ return "";
161
+ }
162
+
163
+ const buttons = [];
164
+
165
+ if (isTextColumn(column)) {
166
+ buttons.push(
167
+ renderPresetButton("text-non-empty", "Non Empty", activePresetId),
168
+ renderPresetButton("text-min-length", "Min Length", activePresetId),
169
+ renderPresetButton("text-max-length", "Max Length", activePresetId),
170
+ renderPresetButton("text-length-range", "Length Range", activePresetId),
171
+ renderPresetButton("text-allowed-values", "Allowed Values", activePresetId)
172
+ );
173
+ }
174
+
175
+ if (isNumericColumn(column)) {
176
+ buttons.push(
177
+ renderPresetButton("numeric-positive", "Positive", activePresetId),
178
+ renderPresetButton("numeric-non-negative", "Non Negative", activePresetId),
179
+ renderPresetButton("numeric-min-value", "Min Value", activePresetId),
180
+ renderPresetButton("numeric-max-value", "Max Value", activePresetId),
181
+ renderPresetButton("numeric-range", "Range", activePresetId),
182
+ renderPresetButton("numeric-non-zero", "Non Zero", activePresetId)
183
+ );
184
+ }
185
+
186
+ if (isBooleanLikeColumn(column)) {
187
+ buttons.push(renderPresetButton("boolean-integer", "Boolean 0 / 1", activePresetId));
188
+ }
189
+
190
+ if (!buttons.length) {
191
+ return "";
192
+ }
193
+
194
+ return `
195
+ <div class="table-designer-check-drawer__quick">
196
+ <div class="table-designer-check-drawer__subheader">
197
+ <span>Quick Checks</span>
198
+ <span class="status-badge status-badge--muted">${escapeHtml(column.type || "TEXT")}</span>
199
+ </div>
200
+ <div class="table-designer-check-drawer__preset-grid">
201
+ ${buttons.join("")}
202
+ </div>
203
+ <p class="table-designer-check-drawer__note">
204
+ CHECK constraints do not replace NOT NULL. Use NOT NULL separately to reject NULL.
205
+ </p>
206
+ </div>
207
+ `;
208
+ }
209
+
210
+ function renderCheckItem(constraint, draft) {
211
+ const expression = normalizeCheckExpression(constraint.expression);
212
+ const canEdit = draft.mode === "create";
213
+
214
+ return `
215
+ <article class="table-designer-check-drawer__item">
216
+ <div class="table-designer-check-drawer__item-header">
217
+ <div>
218
+ <div class="table-designer-check-drawer__item-title">${escapeHtml(
219
+ constraint.name || "CHECK"
220
+ )}</div>
221
+ ${
222
+ constraint.source === "preset"
223
+ ? '<div class="table-designer-check-drawer__item-meta">Preset</div>'
224
+ : ""
225
+ }
226
+ </div>
227
+ ${constraint.originalExpression ? '<span class="status-badge status-badge--muted">Detected</span>' : ""}
228
+ </div>
229
+ <pre class="table-designer-check-drawer__sql custom-scrollbar"><code>${highlightSql(expression)}</code></pre>
230
+ ${
231
+ canEdit
232
+ ? `
233
+ <div class="table-designer-check-drawer__item-actions">
234
+ <button
235
+ class="standard-button"
236
+ data-action="edit-table-designer-check"
237
+ data-constraint-id="${escapeHtml(constraint.id)}"
238
+ type="button"
239
+ >
240
+ Edit
241
+ </button>
242
+ <button
243
+ class="delete-button"
244
+ data-action="remove-table-designer-check"
245
+ data-constraint-id="${escapeHtml(constraint.id)}"
246
+ type="button"
247
+ >
248
+ <span class="material-symbols-outlined text-base">delete</span>
249
+ Remove
250
+ </button>
251
+ </div>
252
+ `
253
+ : ""
254
+ }
255
+ </article>
256
+ `;
257
+ }
258
+
259
+ function renderCheckList(draft, column, checks) {
260
+ return `
261
+ <section class="table-designer-check-drawer__section">
262
+ <div class="table-designer-check-drawer__section-header">
263
+ <div class="table-designer-check-drawer__section-title">
264
+ <span>Check Constraints</span>
265
+ <span class="status-badge status-badge--muted">${formatNumber(checks.length)}</span>
266
+ </div>
267
+ ${
268
+ draft.mode === "create"
269
+ ? `
270
+ <button
271
+ class="standard-button table-designer-check-drawer__header-action"
272
+ data-action="start-table-designer-check-editor"
273
+ type="button"
274
+ >
275
+ <span class="material-symbols-outlined text-base">add</span>
276
+ Add Check
277
+ </button>
278
+ `
279
+ : ""
280
+ }
281
+ </div>
282
+ ${
283
+ checks.length
284
+ ? `<div class="table-designer-check-drawer__list">${checks
285
+ .map((constraint) => renderCheckItem(constraint, draft))
286
+ .join("")}</div>`
287
+ : `
288
+ <div class="table-designer-check-drawer__empty">
289
+ <div>NO CHECK CONSTRAINTS</div>
290
+ <p>CHECK constraints reject rows that do not satisfy a SQL expression.</p>
291
+ </div>
292
+ `
293
+ }
294
+ </section>
295
+ `;
296
+ }
297
+
298
+ function renderEditor(drawer, column) {
299
+ const editor = drawer.editor;
300
+
301
+ if (!editor) {
302
+ return "";
303
+ }
304
+
305
+ const expression = String(editor.expression ?? "");
306
+ const generatedSql = normalizeCheckExpression(expression);
307
+
308
+ return `
309
+ <section class="table-designer-check-drawer__section">
310
+ <div class="table-designer-check-drawer__section-header">
311
+ <div class="table-designer-check-drawer__section-title">
312
+ <span>${drawer.editingConstraintId ? "Edit Check" : "New Check"}</span>
313
+ </div>
314
+ </div>
315
+ ${renderPresetButtons(column, editor.presetId)}
316
+ ${renderPresetParameters(editor)}
317
+ <label class="table-designer-check-drawer__field">
318
+ <span>Custom Expression</span>
319
+ <textarea
320
+ aria-label="CHECK expression"
321
+ class="table-designer-field table-designer-check-drawer__expression custom-scrollbar"
322
+ data-bind="table-designer-check-editor-field"
323
+ data-field="expression"
324
+ spellcheck="false"
325
+ >${escapeHtml(expression)}</textarea>
326
+ </label>
327
+ ${
328
+ editor.error
329
+ ? `
330
+ <div class="table-designer-check-drawer__error" role="alert">
331
+ <div>${escapeHtml(editor.error.code || "INVALID_CHECK_EXPRESSION")}</div>
332
+ <p>${escapeHtml(editor.error.message || "Invalid CHECK expression.")}</p>
333
+ </div>
334
+ `
335
+ : ""
336
+ }
337
+ <div class="table-designer-check-drawer__preview">
338
+ <div>Generated SQL</div>
339
+ <pre class="custom-scrollbar"><code data-table-designer-check-preview>${highlightSql(generatedSql || "CHECK (...)")}</code></pre>
340
+ </div>
341
+ <div class="table-designer-check-drawer__actions">
342
+ <button class="standard-button" data-action="cancel-table-designer-check-editor" type="button">
343
+ Cancel
344
+ </button>
345
+ <button
346
+ class="signature-button"
347
+ data-action="save-table-designer-check"
348
+ type="button"
349
+ ${editor.validating ? "disabled" : ""}
350
+ >
351
+ ${editor.validating ? "Validating..." : "Save Check"}
352
+ </button>
353
+ </div>
354
+ </section>
355
+ `;
356
+ }
357
+
358
+ export function renderTableDesignerConstraintsDrawer(state) {
359
+ const drawer = state.tableDesigner?.constraintsDrawer;
360
+ const draft = state.tableDesigner?.draft;
361
+
362
+ if (!drawer?.visible || !draft) {
363
+ return "";
364
+ }
365
+
366
+ const column =
367
+ (draft.columns ?? []).find((candidate) => candidate.id === drawer.columnId) ?? null;
368
+ const columnLabel = column?.name || drawer.columnName || "Table";
369
+ const checks = getVisibleChecks(draft, column);
370
+
371
+ return `
372
+ <section class="table-designer-check-drawer flex h-full min-h-0 flex-col bg-surface-low">
373
+ <div class="border-b border-outline-variant/10 px-5 py-4">
374
+ <div class="flex items-center justify-between gap-3">
375
+ <div class="min-w-0">
376
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
377
+ Table Designer // ${escapeHtml(columnLabel)}
378
+ </div>
379
+ <h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
380
+ Checks
381
+ </h2>
382
+ </div>
383
+ <button
384
+ class="query-history-icon-button"
385
+ aria-label="Close check constraints"
386
+ data-action="close-table-designer-constraints"
387
+ title="Close check constraints"
388
+ type="button"
389
+ >
390
+ <span class="material-symbols-outlined text-[18px]">close</span>
391
+ </button>
392
+ </div>
393
+ </div>
394
+ <div class="table-designer-check-drawer__body custom-scrollbar">
395
+ ${renderCheckList(draft, column, checks)}
396
+ ${renderEditor(drawer, column)}
397
+ </div>
398
+ </section>
399
+ `;
400
+ }
@@ -85,7 +85,12 @@ function normalizeConstraintColumnName(name) {
85
85
  return String(name ?? "").trim().toLowerCase();
86
86
  }
87
87
 
88
- function constraintIncludesColumn(constraint, columnName) {
88
+ function constraintIncludesColumn(constraint, column) {
89
+ if (constraint.columnId && constraint.columnId === column.id) {
90
+ return true;
91
+ }
92
+
93
+ const columnName = column.name;
89
94
  const normalizedColumn = normalizeConstraintColumnName(columnName);
90
95
 
91
96
  if (!normalizedColumn) {
@@ -98,33 +103,34 @@ function constraintIncludesColumn(constraint, columnName) {
98
103
  }
99
104
 
100
105
  function countColumnCheckConstraints(column, draft) {
101
- if (draft.mode !== "edit") {
102
- return 0;
103
- }
104
-
105
106
  return (draft.checkConstraints ?? []).filter((constraint) =>
106
- constraintIncludesColumn(constraint, column.name)
107
+ !constraint.deleted && constraintIncludesColumn(constraint, column)
107
108
  ).length;
108
109
  }
109
110
 
110
111
  function renderColumnCheckAction(column, draft) {
111
112
  const checkCount = countColumnCheckConstraints(column, draft);
112
113
  const columnName = column.name || "Unnamed column";
114
+ const checkCountLabel = String(checkCount || 0);
113
115
 
114
116
  return `
115
117
  <button
116
118
  aria-label="Checks for ${escapeHtml(columnName)}"
117
119
  class="standard-button table-designer-row-check-button"
118
- data-action="open-modal"
120
+ data-action="open-table-designer-constraints"
119
121
  data-column-id="${escapeHtml(column.id)}"
120
122
  data-column-name="${escapeHtml(column.name)}"
121
- data-modal="table-designer-constraints"
122
123
  title="Checks for ${escapeHtml(columnName)}"
123
124
  type="button"
124
125
  >
125
126
  <span class="material-symbols-outlined text-base">fact_check</span>
126
127
  <span>Checks</span>
127
- ${checkCount ? `<span class="status-badge status-badge--muted">${escapeHtml(String(checkCount))}</span>` : ""}
128
+ <span
129
+ class="status-badge status-badge--muted table-designer-row-check-button__count${checkCount ? "" : " is-empty"}"
130
+ ${checkCount ? "" : 'aria-hidden="true"'}
131
+ >
132
+ ${escapeHtml(checkCountLabel)}
133
+ </span>
128
134
  </button>
129
135
  `;
130
136
  }
@@ -241,12 +247,35 @@ function renderColumnGrid(draft, catalogTables) {
241
247
  `;
242
248
  }
243
249
 
250
+ function getDraftImportRows(draft) {
251
+ return Array.isArray(draft.importRows) && draft.importRows.length
252
+ ? draft.importRows
253
+ : draft.importedCsvRows ?? [];
254
+ }
255
+
256
+ function renderImportSource(draft) {
257
+ const importFormat = String(draft.importFormat ?? "").trim().toUpperCase();
258
+ const sourceFileName = String(draft.importSourceFileName ?? draft.importedCsvFileName ?? "").trim();
259
+
260
+ if (draft.mode !== "create" || !importFormat || !sourceFileName) {
261
+ return "";
262
+ }
263
+
264
+ return `
265
+ <div class="table-designer-import-source">
266
+ <span class="table-designer-import-source__label">SOURCE // ${escapeHtml(importFormat)}</span>
267
+ <span class="table-designer-import-source__file">${escapeHtml(sourceFileName)}</span>
268
+ </div>
269
+ `;
270
+ }
271
+
244
272
  function renderFillToggle(draft) {
245
273
  if (draft.mode !== "create") {
246
274
  return "";
247
275
  }
248
276
 
249
- const hasImportedRows = (draft.importedCsvRows?.length ?? 0) > 0;
277
+ const importRows = getDraftImportRows(draft);
278
+ const hasImportedRows = importRows.length > 0;
250
279
 
251
280
  return `
252
281
  <label class="standard-checkbox table-designer-fill-toggle table-designer-checkbox-override ${hasImportedRows ? "" : "is-disabled"}">
@@ -261,10 +290,10 @@ function renderFillToggle(draft) {
261
290
  <span class="table-designer-fill-toggle__meta">
262
291
  ${
263
292
  hasImportedRows
264
- ? `${escapeHtml(String(draft.importedCsvRows.length))} imported row${
265
- draft.importedCsvRows.length === 1 ? "" : "s"
293
+ ? `${escapeHtml(String(importRows.length))} imported row${
294
+ importRows.length === 1 ? "" : "s"
266
295
  }`
267
- : "Available after CSV import"
296
+ : "Available after data import"
268
297
  }
269
298
  </span>
270
299
  </label>
@@ -323,11 +352,16 @@ export function renderTableDesignerEditor(state) {
323
352
  }
324
353
 
325
354
  const catalogTables = state.tableDesigner.tables ?? [];
355
+ const hasImportedRows = getDraftImportRows(draft).length > 0;
326
356
  const saveLabel =
327
357
  draft.mode === "create"
328
- ? state.tableDesigner.saving
329
- ? "Creating..."
330
- : "Create Table"
358
+ ? hasImportedRows
359
+ ? state.tableDesigner.saving
360
+ ? "Creating & importing..."
361
+ : "Create & Import"
362
+ : state.tableDesigner.saving
363
+ ? "Creating..."
364
+ : "Create Table"
331
365
  : state.tableDesigner.saving
332
366
  ? "Saving..."
333
367
  : "Save Changes";
@@ -355,6 +389,7 @@ export function renderTableDesignerEditor(state) {
355
389
  ? `<div class="status-badge status-badge--primary">CREATE</div>`
356
390
  : ""
357
391
  }
392
+ ${renderImportSource(draft)}
358
393
  </div>
359
394
  <div class="table-designer-main__section-actions">
360
395
  <button
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml, formatNumber } from '../utils/format.js';
2
+ import { renderVirtualTableBadge } from './badges.js';
2
3
 
3
4
  function getFilteredTables(tables, searchQuery) {
4
5
  const normalizedSearch = String(searchQuery ?? '')
@@ -9,11 +10,11 @@ function getFilteredTables(tables, searchQuery) {
9
10
  return tables;
10
11
  }
11
12
 
12
- return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
13
+ return tables.filter(table => !table?.isShadow && table.name.toLowerCase().includes(normalizedSearch));
13
14
  }
14
15
 
15
16
  export function renderTableDesignerSidebar(state) {
16
- const tables = state.tableDesigner.tables ?? [];
17
+ const tables = (state.tableDesigner.tables ?? []).filter(table => !table?.isShadow);
17
18
  const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
18
19
  const isNewDraft = state.tableDesigner.draft?.mode === 'create';
19
20
 
@@ -87,9 +88,12 @@ export function renderTableDesignerSidebar(state) {
87
88
  data-to="/table-designer/${encodeURIComponent(table.name)}"
88
89
  type="button"
89
90
  >
90
- <div class="table-designer-sidebar__item-name ${
91
- !isNewDraft && table.name === state.tableDesigner.selectedTableName ? 'is-active' : ''
92
- }">${escapeHtml(table.name)}</div>
91
+ <div class="flex min-w-0 items-center gap-2">
92
+ <div class="table-designer-sidebar__item-name min-w-0 flex-1 ${
93
+ !isNewDraft && table.name === state.tableDesigner.selectedTableName ? 'is-active' : ''
94
+ }">${escapeHtml(table.name)}</div>
95
+ ${renderVirtualTableBadge(table)}
96
+ </div>
93
97
  <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
94
98
  ${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
95
99
  Number(table.columnCount ?? 0) === 1 ? '' : 's'