sqlite-hub 2.0.1 → 2.2.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 (98) hide show
  1. package/README.md +21 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +16 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/docs/todo.md +2 -0
  6. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  28. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  43. package/frontend/js/api.js +18 -0
  44. package/frontend/js/app.js +567 -15
  45. package/frontend/js/components/badges.js +16 -0
  46. package/frontend/js/components/connectionCard.js +35 -0
  47. package/frontend/js/components/modal.js +268 -247
  48. package/frontend/js/components/queryHistoryDetail.js +25 -45
  49. package/frontend/js/components/rowEditorPanel.js +7 -60
  50. package/frontend/js/components/structureGraph.js +292 -13
  51. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  52. package/frontend/js/components/tableDesignerEditor.js +51 -16
  53. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  54. package/frontend/js/store.js +1847 -190
  55. package/frontend/js/utils/connectionRegistry.js +76 -0
  56. package/frontend/js/utils/markdownDocuments.js +21 -4
  57. package/frontend/js/utils/tableDesigner.js +431 -62
  58. package/frontend/js/views/backups.js +2 -5
  59. package/frontend/js/views/charts.js +20 -10
  60. package/frontend/js/views/connections.js +101 -6
  61. package/frontend/js/views/data.js +52 -9
  62. package/frontend/js/views/documents.js +153 -59
  63. package/frontend/js/views/structure.js +28 -5
  64. package/frontend/js/views/tableAdvisor.js +14 -6
  65. package/frontend/js/views/tableDesigner.js +44 -9
  66. package/frontend/styles/base.css +9 -0
  67. package/frontend/styles/components.css +676 -79
  68. package/frontend/styles/structure-graph.css +49 -0
  69. package/frontend/styles/tailwind.generated.css +1 -1
  70. package/frontend/styles/views.css +12 -0
  71. package/package.json +1 -1
  72. package/server/routes/backups.js +37 -1
  73. package/server/routes/charts.js +28 -0
  74. package/server/routes/connections.js +12 -0
  75. package/server/routes/data.js +30 -2
  76. package/server/routes/documents.js +61 -5
  77. package/server/routes/settings.js +32 -0
  78. package/server/routes/tableDesigner.js +35 -2
  79. package/server/server.js +3 -3
  80. package/server/services/mcpToolService.js +30 -0
  81. package/server/services/sqlite/connectionManager.js +25 -2
  82. package/server/services/sqlite/dataBrowserService.js +21 -0
  83. package/server/services/sqlite/introspection.js +39 -1
  84. package/server/services/sqlite/overviewService.js +1 -0
  85. package/server/services/sqlite/structureService.js +66 -11
  86. package/server/services/sqlite/tableAdvisor.js +6 -0
  87. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  88. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  89. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  90. package/server/services/sqlite/tableDesignerService.js +110 -5
  91. package/server/services/storage/appStateStore.js +590 -10
  92. package/server/utils/fileValidation.js +5 -0
  93. package/server/utils/userActionLog.js +49 -0
  94. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  95. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  96. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  97. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  98. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -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)),
@@ -18,6 +18,11 @@ import {
18
18
  MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
19
19
  } from '../lib/mediaTaggingDefaults.js';
20
20
  import { SYNTHETIC_GENERATOR_TYPES, getSyntheticGeneratorLabel } from '../utils/syntheticData.js';
21
+ import {
22
+ MAX_CONNECTION_TAG_NAME_LENGTH,
23
+ normalizeConnectionTagKey,
24
+ normalizeConnectionTagName,
25
+ } from '../utils/connectionRegistry.js';
21
26
 
22
27
  function renderField({ label, name, type = 'text', placeholder = '', value = '' }) {
23
28
  return `
@@ -192,7 +197,114 @@ export function renderOpenConnectionForm(modal) {
192
197
  `;
193
198
  }
194
199
 
195
- function renderEditConnectionForm(modal) {
200
+ function renderEditConnectionTagEditor(modal, state) {
201
+ const assignedTags = Array.isArray(modal.assignedTags) ? modal.assignedTags : [];
202
+ const assignedKeys = new Set(assignedTags.map(tag => normalizeConnectionTagKey(tag.name)));
203
+ const tagQuery = String(modal.tagQuery ?? '');
204
+ const normalizedQuery = normalizeConnectionTagName(tagQuery);
205
+ const queryKey = normalizeConnectionTagKey(normalizedQuery);
206
+ const matchingTags = (state.connections.tags ?? [])
207
+ .filter(tag => !assignedKeys.has(normalizeConnectionTagKey(tag.name)))
208
+ .filter(tag => {
209
+ if (!queryKey) {
210
+ return true;
211
+ }
212
+
213
+ return normalizeConnectionTagKey(tag.name).includes(queryKey);
214
+ })
215
+ .slice(0, 6);
216
+ const exactMatch = (state.connections.tags ?? []).some(tag => normalizeConnectionTagKey(tag.name) === queryKey);
217
+ const canCreate = Boolean(normalizedQuery && !exactMatch);
218
+ const assignedMarkup = assignedTags.length
219
+ ? assignedTags
220
+ .map(
221
+ tag => `
222
+ <span class="connection-edit-tag-chip">
223
+ <span class="connection-edit-tag-chip__label">${escapeHtml(tag.name)}</span>
224
+ <button
225
+ aria-label="Remove ${escapeHtml(tag.name)} tag"
226
+ class="connection-edit-tag-chip__remove"
227
+ data-action="remove-edit-connection-tag"
228
+ data-tag-name="${escapeHtml(tag.name)}"
229
+ type="button"
230
+ title="Remove tag"
231
+ >
232
+ <span aria-hidden="true">×</span>
233
+ </button>
234
+ <input name="tags" type="hidden" value="${escapeHtml(tag.name)}" />
235
+ </span>
236
+ `,
237
+ )
238
+ .join('')
239
+ : '<div class="connection-edit-tags__empty">NO_TAGS_ASSIGNED</div>';
240
+ const matchMarkup = matchingTags
241
+ .map(
242
+ tag => `
243
+ <button
244
+ class="connection-edit-tag-option"
245
+ data-action="add-edit-connection-tag"
246
+ data-tag-name="${escapeHtml(tag.name)}"
247
+ type="button"
248
+ >
249
+ <span>${escapeHtml(tag.name)}</span>
250
+ <span>${escapeHtml(String(tag.connectionCount ?? 0))}</span>
251
+ </button>
252
+ `,
253
+ )
254
+ .join('');
255
+
256
+ return `
257
+ <div class="connection-edit-tags">
258
+ <div class="connection-edit-tags__label">Tags</div>
259
+ <div class="connection-edit-tags__assigned">
260
+ ${assignedMarkup}
261
+ </div>
262
+ <label class="connection-edit-tags__input-shell">
263
+ <span class="sr-only">Add or create tag</span>
264
+ <input
265
+ autocomplete="off"
266
+ class="control-input connection-edit-tags__input"
267
+ data-bind="edit-connection-tag-query"
268
+ maxlength="${MAX_CONNECTION_TAG_NAME_LENGTH}"
269
+ placeholder="ADD_OR_CREATE_TAG..."
270
+ spellcheck="false"
271
+ type="text"
272
+ value="${escapeHtml(tagQuery)}"
273
+ />
274
+ </label>
275
+ ${
276
+ modal.tagError
277
+ ? `<div class="connection-edit-tags__error">${escapeHtml(modal.tagError)}</div>`
278
+ : ''
279
+ }
280
+ <div class="connection-edit-tag-options">
281
+ ${
282
+ matchMarkup
283
+ ? `<div class="connection-edit-tag-options__section">MATCHING_TAGS</div>${matchMarkup}`
284
+ : ''
285
+ }
286
+ ${
287
+ canCreate
288
+ ? `
289
+ <div class="connection-edit-tag-options__section">CREATE_NEW</div>
290
+ <button
291
+ class="connection-edit-tag-option connection-edit-tag-option--create"
292
+ data-action="create-edit-connection-tag"
293
+ data-edit-connection-tag-primary
294
+ data-tag-name="${escapeHtml(normalizedQuery)}"
295
+ type="button"
296
+ >
297
+ <span>CREATE "${escapeHtml(normalizedQuery)}"</span>
298
+ </button>
299
+ `
300
+ : ''
301
+ }
302
+ </div>
303
+ </div>
304
+ `;
305
+ }
306
+
307
+ function renderEditConnectionForm(modal, state) {
196
308
  const connection = modal.connection ?? {};
197
309
 
198
310
  return `
@@ -246,6 +358,7 @@ function renderEditConnectionForm(modal) {
246
358
  checked: Boolean(connection.readOnly),
247
359
  text: 'Open read-only',
248
360
  })}
361
+ ${renderEditConnectionTagEditor(modal, state)}
249
362
  ${renderError(modal.error)}
250
363
  <div class="flex items-center justify-between gap-3 pt-2">
251
364
  <button
@@ -1473,6 +1586,23 @@ function renderDeleteDocumentForm(modal) {
1473
1586
  ].join('');
1474
1587
  }
1475
1588
 
1589
+ function renderCreateDocumentFolderForm(modal) {
1590
+ return [
1591
+ '<form class="space-y-5" data-form="create-document-folder"><label class="block space-y-2">',
1592
+ '<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Folder Name</span>',
1593
+ '<input class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors placeholder:text-on-surface-variant/35 focus:border-primary-container" name="name" type="text" autocomplete="off" maxlength="80" value="',
1594
+ escapeHtml(modal.name ?? ''),
1595
+ '" autofocus />',
1596
+ '</label>',
1597
+ renderError(modal.error),
1598
+ '<div class="flex items-center justify-between gap-3 pt-2">',
1599
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
1600
+ '<button class="signature-button" type="submit">',
1601
+ modal.submitting ? 'Creating...' : 'Create Folder',
1602
+ '</button></div></form>',
1603
+ ].join('');
1604
+ }
1605
+
1476
1606
  export function renderDeleteApiTokenForm(modal) {
1477
1607
  return [
1478
1608
  '<form class="space-y-5" data-form="delete-api-token-confirm"><div class="space-y-3">',
@@ -1607,13 +1737,136 @@ function renderDocumentInsertNoteForm(modal) {
1607
1737
  <div class="flex items-center justify-between gap-3 pt-2">
1608
1738
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
1609
1739
  <button class="signature-button" type="submit" ${disabledAttribute}>
1610
- ${modal.submitting ? 'Inserting...' : 'Insert Note'}
1740
+ ${modal.submitting ? 'Inserting...' : 'Insert Query Note'}
1611
1741
  </button>
1612
1742
  </div>
1613
1743
  </form>
1614
1744
  `;
1615
1745
  }
1616
1746
 
1747
+ function renderDocumentTableDefinitionSelect(modal) {
1748
+ const tables = (modal.tables ?? []).filter(table => !table.isShadow && table.tableKind !== 'shadow');
1749
+
1750
+ if (modal.loading) {
1751
+ return '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">Loading tables...</div>';
1752
+ }
1753
+
1754
+ if (!tables.length) {
1755
+ return '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">No regular or virtual tables are available for this database.</div>';
1756
+ }
1757
+
1758
+ return `
1759
+ <label class="block space-y-2">
1760
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1761
+ Table
1762
+ </span>
1763
+ <select
1764
+ class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
1765
+ data-bind="document-table-definition-select"
1766
+ name="tableName"
1767
+ >
1768
+ ${tables
1769
+ .map(table => {
1770
+ const label = table.isVirtual || table.tableKind === 'virtual'
1771
+ ? `${table.name} (virtual)`
1772
+ : table.name;
1773
+
1774
+ return `
1775
+ <option
1776
+ value="${escapeHtml(table.name)}"
1777
+ ${String(table.name) === String(modal.selectedTableName) ? 'selected' : ''}
1778
+ >
1779
+ ${escapeHtml(label)}
1780
+ </option>
1781
+ `;
1782
+ })
1783
+ .join('')}
1784
+ </select>
1785
+ </label>
1786
+ `;
1787
+ }
1788
+
1789
+ function renderDocumentTableDefinitionCheckbox({ label, option, checked, disabled }) {
1790
+ return `
1791
+ <label class="standard-checkbox ${disabled ? 'is-disabled' : ''}">
1792
+ <input
1793
+ data-bind="document-table-definition-option"
1794
+ data-option="${escapeHtml(option)}"
1795
+ name="${escapeHtml(option)}"
1796
+ type="checkbox"
1797
+ ${checked ? 'checked' : ''}
1798
+ ${disabled ? 'disabled' : ''}
1799
+ />
1800
+ <span>${escapeHtml(label)}</span>
1801
+ </label>
1802
+ `;
1803
+ }
1804
+
1805
+ function renderDocumentInsertTableDefinitionForm(modal) {
1806
+ const hasTables = Boolean((modal.tables ?? []).length);
1807
+ const optionDisabled = modal.loading || modal.submitting || !hasTables;
1808
+ const hasSelection = Boolean(modal.markdownTable || modal.sqlDefinition || modal.sampleData);
1809
+ const disabledAttribute = optionDisabled || !hasSelection ? 'disabled aria-disabled="true"' : '';
1810
+
1811
+ return `
1812
+ <form class="space-y-5" data-form="document-insert-table-definition">
1813
+ ${renderDocumentTableDefinitionSelect(modal)}
1814
+ <div class="grid gap-2">
1815
+ ${renderDocumentTableDefinitionCheckbox({
1816
+ label: 'Markdown Table',
1817
+ option: 'markdownTable',
1818
+ checked: Boolean(modal.markdownTable),
1819
+ disabled: optionDisabled,
1820
+ })}
1821
+ ${renderDocumentTableDefinitionCheckbox({
1822
+ label: 'SQL Definition',
1823
+ option: 'sqlDefinition',
1824
+ checked: Boolean(modal.sqlDefinition),
1825
+ disabled: optionDisabled,
1826
+ })}
1827
+ ${renderDocumentTableDefinitionCheckbox({
1828
+ label: 'Sample Data',
1829
+ option: 'sampleData',
1830
+ checked: Boolean(modal.sampleData),
1831
+ disabled: optionDisabled,
1832
+ })}
1833
+ </div>
1834
+ ${
1835
+ modal.sampleData
1836
+ ? `
1837
+ <label class="block space-y-2">
1838
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1839
+ Sample Rows
1840
+ </span>
1841
+ <select
1842
+ class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
1843
+ data-bind="document-table-definition-row-count"
1844
+ name="sampleRowCount"
1845
+ ${optionDisabled ? 'disabled' : ''}
1846
+ >
1847
+ ${[3, 5, 10]
1848
+ .map(count => `
1849
+ <option value="${count}" ${Number(modal.sampleRowCount ?? 5) === count ? 'selected' : ''}>
1850
+ ${count}
1851
+ </option>
1852
+ `)
1853
+ .join('')}
1854
+ </select>
1855
+ </label>
1856
+ `
1857
+ : ''
1858
+ }
1859
+ ${renderError(modal.error)}
1860
+ <div class="flex items-center justify-between gap-3 pt-2">
1861
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
1862
+ <button class="signature-button" type="submit" ${disabledAttribute}>
1863
+ ${modal.submitting ? 'Inserting...' : 'Insert Table Definition'}
1864
+ </button>
1865
+ </div>
1866
+ </form>
1867
+ `;
1868
+ }
1869
+
1617
1870
  function renderQueryExportPreview(lines = []) {
1618
1871
  return lines.map(line => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join('');
1619
1872
  }
@@ -1926,242 +2179,6 @@ export function renderCopyColumnModal(modal, state) {
1926
2179
  `;
1927
2180
  }
1928
2181
 
1929
- function getTableDesignerUniqueConstraintTypeLabel(constraint) {
1930
- if (constraint.partial) {
1931
- return 'Partial unique index';
1932
- }
1933
-
1934
- if ((constraint.columns?.length ?? 0) > 1) {
1935
- return 'Multi-column unique';
1936
- }
1937
-
1938
- return 'Unique constraint';
1939
- }
1940
-
1941
- function renderTableDesignerUniqueConstraintExpression(constraint) {
1942
- const expression = String(constraint.expression || constraint.sql || '').trim();
1943
-
1944
- if (expression) {
1945
- return expression;
1946
- }
1947
-
1948
- const columns = (constraint.columns ?? []).map(column => column.name).filter(Boolean);
1949
- return columns.length ? `UNIQUE (${columns.join(', ')})` : 'UNIQUE constraint';
1950
- }
1951
-
1952
- function renderTableDesignerCheckConstraintExpression(constraint) {
1953
- return String(constraint.expression || '').trim() || 'CHECK constraint';
1954
- }
1955
-
1956
- function normalizeTableDesignerConstraintColumnName(name) {
1957
- return String(name ?? '')
1958
- .trim()
1959
- .toLowerCase();
1960
- }
1961
-
1962
- function tableDesignerConstraintIncludesColumn(constraint, columnName) {
1963
- const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1964
-
1965
- if (!normalizedColumn) {
1966
- return false;
1967
- }
1968
-
1969
- return (constraint.columns ?? []).some(
1970
- constraintColumn => normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn,
1971
- );
1972
- }
1973
-
1974
- function renderTableDesignerUniqueConstraintEditor(constraint) {
1975
- const columns = (constraint.columns ?? []).map(column => column.name).filter(Boolean);
1976
-
1977
- return `
1978
- <article class="table-designer-constraint-editor">
1979
- <div class="table-designer-constraint-editor__header">
1980
- <div class="min-w-0">
1981
- <div class="table-designer-constraint-editor__type">
1982
- ${escapeHtml(getTableDesignerUniqueConstraintTypeLabel(constraint))}
1983
- ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ''}
1984
- </div>
1985
- <input
1986
- class="table-designer-field table-designer-constraint-editor__name"
1987
- data-bind="table-designer-constraint-field"
1988
- data-constraint-id="${escapeHtml(constraint.id)}"
1989
- data-constraint-kind="unique"
1990
- data-field="name"
1991
- spellcheck="false"
1992
- type="text"
1993
- value="${escapeHtml(constraint.name || 'UNIQUE constraint')}"
1994
- />
1995
- </div>
1996
- <div class="status-badge status-badge--muted">Rebuild</div>
1997
- </div>
1998
- ${
1999
- columns.length
2000
- ? `
2001
- <div class="table-designer-constraint__columns">
2002
- ${columns.map(column => `<span>${escapeHtml(column)}</span>`).join('')}
2003
- </div>
2004
- `
2005
- : ''
2006
- }
2007
- <textarea
2008
- class="table-designer-constraint-editor__sql custom-scrollbar"
2009
- data-bind="table-designer-constraint-field"
2010
- data-constraint-id="${escapeHtml(constraint.id)}"
2011
- data-constraint-kind="unique"
2012
- data-field="expression"
2013
- spellcheck="false"
2014
- >${escapeHtml(renderTableDesignerUniqueConstraintExpression(constraint))}</textarea>
2015
- </article>
2016
- `;
2017
- }
2018
-
2019
- function renderTableDesignerCheckConstraintEditor(constraint) {
2020
- const columns = constraint.columns ?? [];
2021
- const allowedValues = columns.flatMap(column =>
2022
- (column.allowedValues ?? []).map(value => ({
2023
- columnName: column.name,
2024
- value,
2025
- })),
2026
- );
2027
-
2028
- return `
2029
- <article class="table-designer-constraint-editor">
2030
- <div class="table-designer-constraint-editor__header">
2031
- <div class="min-w-0">
2032
- <div class="table-designer-constraint-editor__type">
2033
- ${columns.length ? escapeHtml(columns.map(column => column.name).join(', ')) : 'Table check'}
2034
- </div>
2035
- <input
2036
- class="table-designer-field table-designer-constraint-editor__name"
2037
- data-bind="table-designer-constraint-field"
2038
- data-constraint-id="${escapeHtml(constraint.id)}"
2039
- data-constraint-kind="check"
2040
- data-field="name"
2041
- spellcheck="false"
2042
- type="text"
2043
- value="${escapeHtml(constraint.name || 'CHECK constraint')}"
2044
- />
2045
- </div>
2046
- <div class="status-badge status-badge--muted">Rebuild</div>
2047
- </div>
2048
- ${
2049
- allowedValues.length
2050
- ? `
2051
- <div class="table-designer-constraint__values custom-scrollbar">
2052
- ${allowedValues
2053
- .map(
2054
- entry => `
2055
- <span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
2056
- `,
2057
- )
2058
- .join('')}
2059
- </div>
2060
- `
2061
- : ''
2062
- }
2063
- <textarea
2064
- class="table-designer-constraint-editor__sql custom-scrollbar"
2065
- data-bind="table-designer-constraint-field"
2066
- data-constraint-id="${escapeHtml(constraint.id)}"
2067
- data-constraint-kind="check"
2068
- data-field="expression"
2069
- spellcheck="false"
2070
- >${escapeHtml(renderTableDesignerCheckConstraintExpression(constraint))}</textarea>
2071
- </article>
2072
- `;
2073
- }
2074
-
2075
- function renderTableDesignerConstraintSection({ title, count, emptyText, body }) {
2076
- return `
2077
- <section class="table-designer-constraints-modal__section">
2078
- <div class="table-designer-constraints-modal__section-header">
2079
- <div class="table-designer-constraints-modal__section-title">${escapeHtml(title)}</div>
2080
- <div class="status-badge status-badge--muted">${formatNumber(count)}</div>
2081
- </div>
2082
- ${
2083
- count
2084
- ? `<div class="table-designer-constraints__list">${body}</div>`
2085
- : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
2086
- }
2087
- </section>
2088
- `;
2089
- }
2090
-
2091
- function renderTableDesignerConstraintsModal(modal, state) {
2092
- const draft = state.tableDesigner?.draft;
2093
-
2094
- if (!draft) {
2095
- return `
2096
- <div class="table-designer-constraints-modal__empty">
2097
- No active table designer draft.
2098
- </div>
2099
- <div class="flex items-center justify-between gap-3 pt-2">
2100
- <button class="standard-button" data-action="close-modal" type="button">Close</button>
2101
- </div>
2102
- `;
2103
- }
2104
-
2105
- const selectedColumn = modal.columnId ? (draft.columns ?? []).find(column => column.id === modal.columnId) : null;
2106
- const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? '').trim();
2107
- const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
2108
- const selectedColumnLabel = selectedColumnName || 'Unnamed column';
2109
- const allUniqueConstraints = draft.mode === 'edit' ? (draft.uniqueConstraints ?? []) : [];
2110
- const allCheckConstraints = draft.mode === 'edit' ? (draft.checkConstraints ?? []) : [];
2111
- const uniqueConstraints = hasSelectedColumn
2112
- ? allUniqueConstraints.filter(constraint =>
2113
- tableDesignerConstraintIncludesColumn(constraint, selectedColumnName),
2114
- )
2115
- : allUniqueConstraints;
2116
- const checkConstraints = hasSelectedColumn
2117
- ? allCheckConstraints.filter(constraint =>
2118
- tableDesignerConstraintIncludesColumn(constraint, selectedColumnName),
2119
- )
2120
- : allCheckConstraints;
2121
- const totalCount = uniqueConstraints.length + checkConstraints.length;
2122
-
2123
- return `
2124
- <div class="table-designer-constraints-modal">
2125
- <div class="table-designer-constraints-modal__summary">
2126
- <div class="min-w-0">
2127
- <div class="table-designer-constraints-modal__label">Table</div>
2128
- <code title="${escapeHtml(draft.tableName ?? '')}">${escapeHtml(draft.tableName ?? '')}</code>
2129
- </div>
2130
- ${
2131
- hasSelectedColumn
2132
- ? `
2133
- <div class="min-w-0">
2134
- <div class="table-designer-constraints-modal__label">Column</div>
2135
- <code title="${escapeHtml(selectedColumnLabel)}">${escapeHtml(selectedColumnLabel)}</code>
2136
- </div>
2137
- `
2138
- : ''
2139
- }
2140
- <div class="status-badge status-badge--primary">V2 · ${formatNumber(totalCount)}</div>
2141
- </div>
2142
- ${renderTableDesignerConstraintSection({
2143
- title: 'Check constraints',
2144
- count: checkConstraints.length,
2145
- emptyText: hasSelectedColumn
2146
- ? 'No check constraints detected for this column.'
2147
- : 'No check constraints detected.',
2148
- body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(''),
2149
- })}
2150
- ${renderTableDesignerConstraintSection({
2151
- title: hasSelectedColumn ? 'Related unique constraints' : 'Unique constraints',
2152
- count: uniqueConstraints.length,
2153
- emptyText: hasSelectedColumn
2154
- ? 'No related multi-column or partial unique constraints detected.'
2155
- : 'No multi-column or partial unique constraints detected.',
2156
- body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(''),
2157
- })}
2158
- <div class="flex items-center justify-between gap-3 pt-2">
2159
- <button class="standard-button" data-action="close-modal" type="button">Close</button>
2160
- </div>
2161
- </div>
2162
- `;
2163
- }
2164
-
2165
2182
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
2166
2183
  const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
2167
2184
  const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
@@ -2346,7 +2363,7 @@ export function renderModal(state) {
2346
2363
  'edit-connection': {
2347
2364
  eyebrow: 'Registry // Update saved SQLite target',
2348
2365
  title: 'Edit Connection',
2349
- body: renderEditConnectionForm(modal),
2366
+ body: renderEditConnectionForm(modal, state),
2350
2367
  },
2351
2368
  'delete-row': {
2352
2369
  eyebrow: 'Mutation // Confirm row deletion',
@@ -2378,6 +2395,11 @@ export function renderModal(state) {
2378
2395
  title: 'Delete Document',
2379
2396
  body: renderDeleteDocumentForm(modal),
2380
2397
  },
2398
+ 'create-document-folder': {
2399
+ eyebrow: 'Documents // New folder',
2400
+ title: 'New Folder',
2401
+ body: renderCreateDocumentFolderForm(modal),
2402
+ },
2381
2403
  'delete-api-token': {
2382
2404
  eyebrow: 'Settings // Confirm token deletion',
2383
2405
  title: 'Delete API Token',
@@ -2390,9 +2412,14 @@ export function renderModal(state) {
2390
2412
  },
2391
2413
  'document-insert-note': {
2392
2414
  eyebrow: 'Documents // Saved query notes',
2393
- title: 'Insert Note',
2415
+ title: 'Insert Query Note',
2394
2416
  body: renderDocumentInsertNoteForm(modal),
2395
2417
  },
2418
+ 'document-insert-table-definition': {
2419
+ eyebrow: 'Documents // Table definition',
2420
+ title: 'Insert Table Definition',
2421
+ body: renderDocumentInsertTableDefinitionForm(modal),
2422
+ },
2396
2423
  'query-export': {
2397
2424
  eyebrow: 'SQL Editor // Export query result',
2398
2425
  title: 'Export Query',
@@ -2408,11 +2435,6 @@ export function renderModal(state) {
2408
2435
  title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? 'Export Markdown Todo' : 'Copy column',
2409
2436
  body: renderCopyColumnModal(modal, state),
2410
2437
  },
2411
- 'table-designer-constraints': {
2412
- eyebrow: 'Table Designer // Constraints',
2413
- title: 'Checks',
2414
- body: renderTableDesignerConstraintsModal(modal, state),
2415
- },
2416
2438
  'create-media-tagging-tag-table': {
2417
2439
  eyebrow: 'Media Tagging // Create default tag table',
2418
2440
  title: 'Create Tag Table',
@@ -2442,8 +2464,7 @@ export function renderModal(state) {
2442
2464
  modal.kind === 'chart-editor' ||
2443
2465
  modal.kind === 'document-insert-table' ||
2444
2466
  modal.kind === 'document-insert-note' ||
2445
- modal.kind === 'row-update-preview' ||
2446
- modal.kind === 'table-designer-constraints'
2467
+ modal.kind === 'row-update-preview'
2447
2468
  ? 'max-w-3xl'
2448
2469
  : modal.kind === 'generate-types' || modal.kind === 'generate-data'
2449
2470
  ? 'max-w-6xl'