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
@@ -35,6 +35,7 @@ import {
35
35
  clearEditorRowSelection,
36
36
  clearEditorResults,
37
37
  clearQueryHistorySelection,
38
+ clearConnectionTagFilters,
38
39
  closeModal,
39
40
  checkSettingsAppVersion,
40
41
  dismissMediaTaggingIssue,
@@ -97,12 +98,21 @@ import {
97
98
  selectQueryHistoryItem,
98
99
  selectStructureEntry,
99
100
  setDocumentsSearchQuery,
101
+ setConnectionSearchQuery,
100
102
  setLogFilter,
101
103
  setLogSearchInput,
102
104
  applyLogSearch,
103
105
  setTableDesignerSearchQuery,
104
106
  setTableDesignerSqlPreviewVisibility,
105
107
  toggleTableDesignerTablesPanel,
108
+ openTableDesignerConstraintsDrawer,
109
+ closeTableDesignerConstraintsDrawer,
110
+ startTableDesignerCheckEditor,
111
+ cancelTableDesignerCheckEditor,
112
+ applyTableDesignerCheckPreset,
113
+ updateTableDesignerCheckEditorField,
114
+ saveTableDesignerCheckEditor,
115
+ removeCurrentTableDesignerCheckConstraint,
106
116
  setDataTableSearchQuery,
107
117
  setStructureTableSearchQuery,
108
118
  toggleStructureTablesPanel,
@@ -156,7 +166,7 @@ import {
156
166
  toggleDocumentsPane,
157
167
  setMediaTaggingWorkflowMediaDetailsVisible,
158
168
  setMediaTaggingWorkflowMediaRotationDegrees,
159
- queueTableDesignerCsvImport,
169
+ queueTableDesignerImport,
160
170
  showToast,
161
171
  storeCopyColumnPreferences,
162
172
  submitCreateConnection,
@@ -167,7 +177,9 @@ import {
167
177
  submitOpenConnection,
168
178
  subscribe,
169
179
  toggleCurrentMediaTagSelection,
180
+ toggleConnectionTagFilter,
170
181
  toggleQueryHistorySavedState,
182
+ addEditConnectionTag,
171
183
  updateCurrentMediaTaggingField,
172
184
  updateCurrentMediaTaggingTagFormField,
173
185
  updateCopyColumnModalFormatField,
@@ -181,6 +193,8 @@ import {
181
193
  updateGenerateDataMapping,
182
194
  updateGenerateDataModal,
183
195
  updateGenerateTypesModal,
196
+ updateEditConnectionTagQuery,
197
+ removeEditConnectionTag,
184
198
  addCurrentTableDesignerColumn,
185
199
  applyCurrentMediaTaggingSelection,
186
200
  removeCurrentTableDesignerColumn,
@@ -271,6 +285,20 @@ let pendingDocumentAutosaveId = null;
271
285
  const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
272
286
 
273
287
  const APP_TITLE = 'SQLite Hub';
288
+ const TABLE_DESIGNER_IMPORT_FORMATS = {
289
+ csv: {
290
+ accept: '.csv,text/csv',
291
+ label: 'CSV',
292
+ },
293
+ tsv: {
294
+ accept: '.tsv,text/tab-separated-values',
295
+ label: 'TSV',
296
+ },
297
+ json: {
298
+ accept: '.json,application/json',
299
+ label: 'JSON',
300
+ },
301
+ };
274
302
  const ROUTE_TITLE_SEGMENTS = {
275
303
  connections: 'Connections',
276
304
  backups: 'Backups',
@@ -575,6 +603,28 @@ function syncTableDesignerDraftUi(sourceNode) {
575
603
  return synced;
576
604
  }
577
605
 
606
+ function normalizeCheckExpressionPreview(expression) {
607
+ const normalized = String(expression ?? '').trim();
608
+
609
+ if (!normalized) {
610
+ return 'CHECK (...)';
611
+ }
612
+
613
+ return /^CHECK\s*\(/i.test(normalized) ? normalized : `CHECK (${normalized})`;
614
+ }
615
+
616
+ function syncTableDesignerCheckEditorPreview(sourceNode) {
617
+ const drawerNode = sourceNode.closest('.table-designer-check-drawer');
618
+ const previewNode = drawerNode?.querySelector('[data-table-designer-check-preview]');
619
+
620
+ if (!(previewNode instanceof HTMLElement)) {
621
+ return false;
622
+ }
623
+
624
+ replaceChildrenFromRenderedMarkup(previewNode, highlightSql(normalizeCheckExpressionPreview(sourceNode.value)));
625
+ return true;
626
+ }
627
+
578
628
  function syncDataRowSelectionUi(selectedRowIndex = null) {
579
629
  if (getState().route.name !== 'data') {
580
630
  return false;
@@ -1417,11 +1467,18 @@ async function applyMediaTaggingAndFocusSearch() {
1417
1467
  return result;
1418
1468
  }
1419
1469
 
1420
- async function handleTableDesignerCsvImport(fileInput) {
1470
+ function normalizeTableDesignerImportFormat(value) {
1471
+ const normalized = String(value ?? '').trim().toLowerCase();
1472
+ return Object.prototype.hasOwnProperty.call(TABLE_DESIGNER_IMPORT_FORMATS, normalized) ? normalized : 'csv';
1473
+ }
1474
+
1475
+ async function handleTableDesignerImport(fileInput) {
1421
1476
  if (!(fileInput instanceof HTMLInputElement)) {
1422
1477
  return;
1423
1478
  }
1424
1479
 
1480
+ const format = normalizeTableDesignerImportFormat(fileInput.dataset.importFormat);
1481
+ const formatLabel = TABLE_DESIGNER_IMPORT_FORMATS[format].label;
1425
1482
  const file = fileInput.files?.[0];
1426
1483
 
1427
1484
  if (!(file instanceof File)) {
@@ -1429,14 +1486,14 @@ async function handleTableDesignerCsvImport(fileInput) {
1429
1486
  }
1430
1487
 
1431
1488
  if (!file.size) {
1432
- showToast('The selected CSV file is empty.', 'alert');
1489
+ showToast(`The selected ${formatLabel} file is empty.`, 'alert');
1433
1490
  fileInput.value = '';
1434
1491
  return;
1435
1492
  }
1436
1493
 
1437
1494
  try {
1438
- const csvText = await readFileAsText(file);
1439
- const imported = queueTableDesignerCsvImport(file.name, csvText);
1495
+ const text = await readFileAsText(file);
1496
+ const imported = queueTableDesignerImport(file.name, text, { format });
1440
1497
 
1441
1498
  if (!imported) {
1442
1499
  return;
@@ -1450,7 +1507,7 @@ async function handleTableDesignerCsvImport(fileInput) {
1450
1507
  'success',
1451
1508
  );
1452
1509
  } catch (error) {
1453
- showToast(error?.message || 'CSV import failed.', 'alert');
1510
+ showToast(error?.message || `${formatLabel} import failed.`, 'alert');
1454
1511
  } finally {
1455
1512
  fileInput.value = '';
1456
1513
  }
@@ -2680,6 +2737,16 @@ async function handleAction(actionNode) {
2680
2737
  case 'edit-connection':
2681
2738
  openEditConnectionModal(actionNode.dataset.connectionId);
2682
2739
  return;
2740
+ case 'clear-connection-tag-filters':
2741
+ clearConnectionTagFilters();
2742
+ return;
2743
+ case 'add-edit-connection-tag':
2744
+ case 'create-edit-connection-tag':
2745
+ addEditConnectionTag(actionNode.dataset.tagName);
2746
+ return;
2747
+ case 'remove-edit-connection-tag':
2748
+ removeEditConnectionTag(actionNode.dataset.tagName);
2749
+ return;
2683
2750
  case 'choose-open-database-path': {
2684
2751
  const labelNode = actionNode.querySelector('[data-open-database-path-button-label]');
2685
2752
 
@@ -3063,6 +3130,30 @@ async function handleAction(actionNode) {
3063
3130
  removeCurrentTableDesignerColumn(actionNode.dataset.columnId);
3064
3131
  }
3065
3132
  return;
3133
+ case 'open-table-designer-constraints':
3134
+ openTableDesignerConstraintsDrawer(actionNode.dataset.columnId, actionNode.dataset.columnName);
3135
+ return;
3136
+ case 'close-table-designer-constraints':
3137
+ closeTableDesignerConstraintsDrawer();
3138
+ return;
3139
+ case 'start-table-designer-check-editor':
3140
+ startTableDesignerCheckEditor();
3141
+ return;
3142
+ case 'edit-table-designer-check':
3143
+ startTableDesignerCheckEditor(actionNode.dataset.constraintId);
3144
+ return;
3145
+ case 'cancel-table-designer-check-editor':
3146
+ cancelTableDesignerCheckEditor();
3147
+ return;
3148
+ case 'apply-table-designer-check-preset':
3149
+ applyTableDesignerCheckPreset(actionNode.dataset.presetId);
3150
+ return;
3151
+ case 'save-table-designer-check':
3152
+ await saveTableDesignerCheckEditor();
3153
+ return;
3154
+ case 'remove-table-designer-check':
3155
+ removeCurrentTableDesignerCheckConstraint(actionNode.dataset.constraintId);
3156
+ return;
3066
3157
  case 'save-table-designer': {
3067
3158
  const savedTableName = await saveCurrentTableDesignerDraft();
3068
3159
  if (savedTableName) {
@@ -3186,14 +3277,20 @@ async function handleAction(actionNode) {
3186
3277
  router.navigate(`/structure/${encodeURIComponent(mediaTableName)}`);
3187
3278
  return;
3188
3279
  }
3189
- case 'import-table-designer-csv': {
3280
+ case 'import-table-designer-data': {
3190
3281
  const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
3191
3282
 
3192
3283
  if (!(fileInput instanceof HTMLInputElement)) {
3193
3284
  return;
3194
3285
  }
3195
3286
 
3287
+ const format = normalizeTableDesignerImportFormat(actionNode.dataset.importFormat);
3288
+ const formatOptions = TABLE_DESIGNER_IMPORT_FORMATS[format];
3289
+
3290
+ fileInput.dataset.importFormat = format;
3291
+ fileInput.accept = actionNode.dataset.importAccept || formatOptions.accept;
3196
3292
  fileInput.value = '';
3293
+ actionNode.closest('[data-dropdown-button]')?.removeAttribute('open');
3197
3294
  fileInput.click();
3198
3295
  return;
3199
3296
  }
@@ -3256,6 +3353,35 @@ function canApplyMediaTaggingShortcut(state) {
3256
3353
  );
3257
3354
  }
3258
3355
 
3356
+ function isEditableShortcutTarget(target) {
3357
+ if (!(target instanceof HTMLElement)) {
3358
+ return false;
3359
+ }
3360
+
3361
+ if (
3362
+ target instanceof HTMLInputElement ||
3363
+ target instanceof HTMLTextAreaElement ||
3364
+ target instanceof HTMLSelectElement
3365
+ ) {
3366
+ return true;
3367
+ }
3368
+
3369
+ if (target.isContentEditable) {
3370
+ return true;
3371
+ }
3372
+
3373
+ if (
3374
+ target.closest(
3375
+ '[contenteditable="true"], [data-bind="current-query"], [data-sql-highlight="true"], .query-editor-input',
3376
+ )
3377
+ ) {
3378
+ return true;
3379
+ }
3380
+
3381
+ const role = String(target.getAttribute('role') ?? '').trim().toLowerCase();
3382
+ return role === 'textbox' || role === 'searchbox' || role === 'combobox';
3383
+ }
3384
+
3259
3385
  document.addEventListener('click', event => {
3260
3386
  const target = event.target instanceof Element ? event.target : null;
3261
3387
 
@@ -3340,6 +3466,27 @@ document.addEventListener('keydown', event => {
3340
3466
  return;
3341
3467
  }
3342
3468
 
3469
+ if (
3470
+ event.key === '/' &&
3471
+ !event.shiftKey &&
3472
+ !event.altKey &&
3473
+ !event.ctrlKey &&
3474
+ !event.metaKey &&
3475
+ !event.defaultPrevented &&
3476
+ state.route.name === 'connections' &&
3477
+ !isEditableShortcutTarget(target)
3478
+ ) {
3479
+ const searchInput = document.querySelector('[data-connections-search-input]');
3480
+
3481
+ if (searchInput instanceof HTMLInputElement) {
3482
+ event.preventDefault();
3483
+ searchInput.focus({ preventScroll: true });
3484
+ searchInput.select();
3485
+ }
3486
+
3487
+ return;
3488
+ }
3489
+
3343
3490
  // Handle Enter key in tag form fields to trigger create tag
3344
3491
  if (
3345
3492
  event.key === 'Enter' &&
@@ -3359,6 +3506,34 @@ document.addEventListener('keydown', event => {
3359
3506
  return;
3360
3507
  }
3361
3508
 
3509
+ if (
3510
+ event.key === 'Enter' &&
3511
+ !event.shiftKey &&
3512
+ !event.altKey &&
3513
+ !event.ctrlKey &&
3514
+ !event.metaKey &&
3515
+ !event.defaultPrevented &&
3516
+ target instanceof HTMLElement &&
3517
+ target.dataset.bind === 'edit-connection-tag-query'
3518
+ ) {
3519
+ event.preventDefault();
3520
+ const tagQuery = target instanceof HTMLInputElement ? target.value.trim() : '';
3521
+
3522
+ if (!tagQuery) {
3523
+ return;
3524
+ }
3525
+
3526
+ const primaryTagAction =
3527
+ document.querySelector('[data-edit-connection-tag-primary]') ??
3528
+ document.querySelector('[data-action="add-edit-connection-tag"]');
3529
+
3530
+ if (primaryTagAction instanceof HTMLElement) {
3531
+ primaryTagAction.click();
3532
+ }
3533
+
3534
+ return;
3535
+ }
3536
+
3362
3537
  if (
3363
3538
  (event.key === 'Enter' || event.code === 'Enter' || event.code === 'NumpadEnter') &&
3364
3539
  event.shiftKey &&
@@ -3396,6 +3571,16 @@ document.addEventListener('keydown', event => {
3396
3571
  return;
3397
3572
  }
3398
3573
 
3574
+ if (target instanceof HTMLInputElement && target.dataset.bind === 'connections-search') {
3575
+ event.preventDefault();
3576
+
3577
+ if (!clearInputForEscape(target)) {
3578
+ target.blur();
3579
+ }
3580
+
3581
+ return;
3582
+ }
3583
+
3399
3584
  if (target instanceof HTMLInputElement && clearInputForEscape(target)) {
3400
3585
  event.preventDefault();
3401
3586
  return;
@@ -3425,6 +3610,12 @@ document.addEventListener('keydown', event => {
3425
3610
  return;
3426
3611
  }
3427
3612
 
3613
+ if (state.route.name === 'tableDesigner' && state.tableDesigner.constraintsDrawer?.visible) {
3614
+ event.preventDefault();
3615
+ closeTableDesignerConstraintsDrawer();
3616
+ return;
3617
+ }
3618
+
3428
3619
  if (state.route.name === 'charts' && state.charts.detailPanelVisible) {
3429
3620
  event.preventDefault();
3430
3621
  setChartsDetailPanelVisibility(false);
@@ -3538,6 +3729,16 @@ document.addEventListener('input', event => {
3538
3729
  return;
3539
3730
  }
3540
3731
 
3732
+ if (bindNode.dataset.bind === 'connections-search') {
3733
+ setConnectionSearchQuery(bindNode.value);
3734
+ return;
3735
+ }
3736
+
3737
+ if (bindNode.dataset.bind === 'edit-connection-tag-query') {
3738
+ updateEditConnectionTagQuery(bindNode.value);
3739
+ return;
3740
+ }
3741
+
3541
3742
  if (bindNode.dataset.bind === 'data-search-query') {
3542
3743
  void setDataSearchQuery(bindNode.value);
3543
3744
  return;
@@ -3563,6 +3764,20 @@ document.addEventListener('input', event => {
3563
3764
  return;
3564
3765
  }
3565
3766
 
3767
+ if (bindNode.dataset.bind === 'table-designer-check-editor-field') {
3768
+ const field = bindNode.dataset.field;
3769
+ const value = bindNode instanceof HTMLInputElement || bindNode instanceof HTMLTextAreaElement ? bindNode.value : '';
3770
+ const isCustomExpression = field === 'expression';
3771
+
3772
+ updateTableDesignerCheckEditorField(field, value, { notify: !isCustomExpression });
3773
+
3774
+ if (isCustomExpression) {
3775
+ syncTableDesignerCheckEditorPreview(bindNode);
3776
+ }
3777
+
3778
+ return;
3779
+ }
3780
+
3566
3781
  if (bindNode.dataset.bind === 'table-designer-field') {
3567
3782
  if (
3568
3783
  (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') ||
@@ -3709,6 +3924,12 @@ document.addEventListener('change', event => {
3709
3924
  return;
3710
3925
  }
3711
3926
 
3927
+ if (bindNode.dataset.bind === 'connection-tag-filter') {
3928
+ const nextValue = bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : false;
3929
+ toggleConnectionTagFilter(bindNode.dataset.tagId, nextValue);
3930
+ return;
3931
+ }
3932
+
3712
3933
  if (bindNode.dataset.bind === 'media-tagging-field') {
3713
3934
  updateCurrentMediaTaggingField(bindNode.dataset.field, bindNode.value);
3714
3935
  void refreshMediaTaggingPreview();
@@ -3731,7 +3952,15 @@ document.addEventListener('change', event => {
3731
3952
  }
3732
3953
 
3733
3954
  if (bindNode.dataset.bind === 'table-designer-import-file') {
3734
- void handleTableDesignerCsvImport(bindNode);
3955
+ void handleTableDesignerImport(bindNode);
3956
+ return;
3957
+ }
3958
+
3959
+ if (bindNode.dataset.bind === 'table-designer-check-editor-field') {
3960
+ const value = bindNode instanceof HTMLInputElement || bindNode instanceof HTMLTextAreaElement ? bindNode.value : '';
3961
+
3962
+ updateTableDesignerCheckEditorField(bindNode.dataset.field, value, { notify: false });
3963
+ syncTableDesignerCheckEditorPreview(bindNode);
3735
3964
  return;
3736
3965
  }
3737
3966
 
@@ -3913,6 +4142,7 @@ document.addEventListener('submit', async event => {
3913
4142
  readOnly: formData.get('readOnly') === 'on',
3914
4143
  clearLogo: formData.get('clearLogo') === 'on' && !logoUpload,
3915
4144
  logoUpload,
4145
+ tags: formData.getAll('tags').map(value => String(value ?? '')),
3916
4146
  });
3917
4147
 
3918
4148
  return;
@@ -3,3 +3,19 @@ import { escapeHtml } from "../utils/format.js";
3
3
  export function renderStatusBadge(label, tone = "muted") {
4
4
  return `<span class="status-badge status-badge--${tone}">${escapeHtml(label)}</span>`;
5
5
  }
6
+
7
+ export function renderTableKindBadge(table) {
8
+ if (table?.isVirtual) {
9
+ return `<span class="status-badge status-badge--primary shrink-0" title="Virtual table">Virtual</span>`;
10
+ }
11
+
12
+ if (table?.isShadow) {
13
+ return `<span class="status-badge status-badge--muted shrink-0" title="Shadow table">Shadow</span>`;
14
+ }
15
+
16
+ return "";
17
+ }
18
+
19
+ export function renderVirtualTableBadge(table) {
20
+ return renderTableKindBadge(table);
21
+ }
@@ -7,6 +7,40 @@ import {
7
7
  import { renderStatusBadge } from "./badges.js";
8
8
  import { renderConnectionLogo } from "./connectionLogo.js";
9
9
 
10
+ function renderConnectionTags(tags = []) {
11
+ const normalizedTags = Array.isArray(tags)
12
+ ? tags.filter((tag) => String(tag?.name ?? "").trim())
13
+ : [];
14
+
15
+ if (!normalizedTags.length) {
16
+ return "";
17
+ }
18
+
19
+ const visibleTags = normalizedTags.slice(0, 2);
20
+ const hiddenCount = Math.max(0, normalizedTags.length - visibleTags.length);
21
+ const title = normalizedTags.map((tag) => tag.name).join(", ");
22
+
23
+ return [
24
+ '<div class="connection-user-tags" title="',
25
+ escapeHtml(title),
26
+ '">',
27
+ visibleTags
28
+ .map(
29
+ (tag) =>
30
+ '<span class="connection-user-tag">' +
31
+ escapeHtml(tag.name) +
32
+ "</span>"
33
+ )
34
+ .join(""),
35
+ hiddenCount
36
+ ? '<span class="connection-user-tag connection-user-tag--more">+' +
37
+ escapeHtml(String(hiddenCount)) +
38
+ "</span>"
39
+ : "",
40
+ "</div>",
41
+ ].join("");
42
+ }
43
+
10
44
  export function renderConnectionCard(connection, activeConnectionId) {
11
45
  const isActive = activeConnectionId
12
46
  ? connection.id === activeConnectionId
@@ -46,6 +80,7 @@ export function renderConnectionCard(connection, activeConnectionId) {
46
80
  '">',
47
81
  escapeHtml(truncateMiddle(connection.path, 68)),
48
82
  "</p>",
83
+ renderConnectionTags(connection.tags),
49
84
  '<div class="mt-8 grid grid-cols-2 gap-4">',
50
85
  '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div><div class="text-xs font-bold text-on-surface">',
51
86
  escapeHtml(formatBytes(connection.sizeBytes)),