sqlite-hub 1.0.0 → 1.1.1

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 (47) hide show
  1. package/README.md +47 -33
  2. package/bin/sqlite-hub.js +127 -47
  3. package/frontend/js/api.js +6 -0
  4. package/frontend/js/app.js +229 -44
  5. package/frontend/js/components/modal.js +17 -1
  6. package/frontend/js/components/queryChartRenderer.js +28 -3
  7. package/frontend/js/components/queryEditor.js +24 -26
  8. package/frontend/js/components/queryHistoryDetail.js +1 -1
  9. package/frontend/js/components/queryHistoryHeader.js +48 -0
  10. package/frontend/js/components/queryHistoryList.js +132 -0
  11. package/frontend/js/components/queryHistoryPanel.js +72 -136
  12. package/frontend/js/components/structureGraph.js +700 -89
  13. package/frontend/js/components/tableDesignerEditor.js +26 -47
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/store.js +97 -8
  17. package/frontend/js/utils/exportFilenames.js +3 -1
  18. package/frontend/js/views/charts.js +326 -174
  19. package/frontend/js/views/connections.js +59 -64
  20. package/frontend/js/views/data.js +48 -54
  21. package/frontend/js/views/documents.js +62 -23
  22. package/frontend/js/views/editor.js +0 -2
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/settings.js +78 -0
  25. package/frontend/js/views/structure.js +48 -32
  26. package/frontend/js/views/tableDesigner.js +45 -1
  27. package/frontend/styles/components.css +259 -54
  28. package/frontend/styles/structure-graph.css +150 -37
  29. package/frontend/styles/tailwind.generated.css +1 -1
  30. package/frontend/styles/tokens.css +2 -0
  31. package/frontend/styles/views.css +75 -38
  32. package/package.json +3 -2
  33. package/server/routes/export.js +89 -22
  34. package/server/routes/externalApi.js +84 -2
  35. package/server/routes/settings.js +37 -28
  36. package/server/services/appInfoService.js +215 -0
  37. package/server/services/databaseCommandService.js +50 -6
  38. package/server/services/sqlite/dataBrowserService.js +11 -3
  39. package/server/services/sqlite/exportService.js +307 -22
  40. package/tests/api-token-auth.test.js +110 -1
  41. package/tests/cli-args.test.js +16 -3
  42. package/tests/database-command-service.test.js +40 -3
  43. package/tests/export-blob.test.js +54 -1
  44. package/tests/export-filenames.test.js +4 -0
  45. package/tests/settings-api-tokens-route.test.js +22 -1
  46. package/tests/settings-metadata.test.js +99 -1
  47. package/tests/settings-view.test.js +28 -0
@@ -323,7 +323,6 @@ export function renderTableDesignerEditor(state) {
323
323
  }
324
324
 
325
325
  const catalogTables = state.tableDesigner.tables ?? [];
326
- const visibleColumns = draft.columns.filter((column) => !column.deleted);
327
326
  const saveLabel =
328
327
  draft.mode === "create"
329
328
  ? state.tableDesigner.saving
@@ -335,11 +334,12 @@ export function renderTableDesignerEditor(state) {
335
334
 
336
335
  return `
337
336
  <section class="table-designer-main shell-section">
338
- <header class="table-designer-main__header">
339
- <div>
340
- <div class="table-designer-main__eyebrow">
341
- ${draft.mode === "create" ? "New Table Draft" : "Editing Existing Table"}
342
- </div>
337
+ <div data-table-designer-feedback>
338
+ ${renderTableDesignerFeedback(draft, state.tableDesigner.saveError)}
339
+ </div>
340
+
341
+ <section class="table-designer-main__section">
342
+ <div class="table-designer-main__section-header">
343
343
  <div class="table-designer-main__title-row">
344
344
  <input
345
345
  class="table-designer-main__name"
@@ -351,49 +351,11 @@ export function renderTableDesignerEditor(state) {
351
351
  value="${escapeHtml(draft.tableName)}"
352
352
  />
353
353
  ${
354
- draft.mode === "edit"
355
- ? `<div class="status-badge status-badge--muted">FROM ${escapeHtml(
356
- draft.originalTableName
357
- )}</div>`
358
- : `<div class="status-badge status-badge--primary">CREATE</div>`
354
+ draft.mode === "create"
355
+ ? `<div class="status-badge status-badge--primary">CREATE</div>`
356
+ : ""
359
357
  }
360
358
  </div>
361
- <div class="table-designer-main__subtitle">
362
- ${escapeHtml(String(visibleColumns.length))} visible column${
363
- visibleColumns.length === 1 ? "" : "s"
364
- } // Table Designer v2 // SQLite-safe operations only
365
- </div>
366
- </div>
367
- <div class="table-designer-main__actions">
368
- ${renderFillToggle(draft)}
369
- <button
370
- class="standard-button"
371
- data-action="refresh-view"
372
- type="button"
373
- >
374
- Reload Schema
375
- </button>
376
- <button
377
- class="standard-button"
378
- data-action="save-table-designer"
379
- data-table-designer-save-button
380
- ${draft.canSave ? "" : "disabled"}
381
- type="button"
382
- >
383
- ${escapeHtml(saveLabel)}
384
- </button>
385
- </div>
386
- </header>
387
-
388
- <div data-table-designer-feedback>
389
- ${renderTableDesignerFeedback(draft, state.tableDesigner.saveError)}
390
- </div>
391
-
392
- <section class="table-designer-main__section">
393
- <div class="table-designer-main__section-header">
394
- <div>
395
- <div class="table-designer-main__section-title">Columns</div>
396
- </div>
397
359
  <div class="table-designer-main__section-actions">
398
360
  <button
399
361
  class="standard-button"
@@ -402,6 +364,23 @@ export function renderTableDesignerEditor(state) {
402
364
  >
403
365
  + Add Column
404
366
  </button>
367
+ <button
368
+ class="standard-button"
369
+ data-action="refresh-view"
370
+ type="button"
371
+ >
372
+ Reload Schema
373
+ </button>
374
+ <button
375
+ class="standard-button"
376
+ data-action="save-table-designer"
377
+ data-table-designer-save-button
378
+ ${draft.canSave ? "" : "disabled"}
379
+ type="button"
380
+ >
381
+ ${escapeHtml(saveLabel)}
382
+ </button>
383
+ ${renderFillToggle(draft)}
405
384
  </div>
406
385
  </div>
407
386
  ${renderColumnGrid(draft, catalogTables)}
@@ -18,38 +18,14 @@ export function renderTableDesignerSidebar(state) {
18
18
  const isNewDraft = state.tableDesigner.draft?.mode === 'create';
19
19
 
20
20
  return `
21
- <aside class="table-designer-sidebar">
22
- <div class="table-designer-sidebar__header">
21
+ <aside class="table-designer-sidebar subnavi-panel">
22
+ <div class="table-designer-sidebar__header subnavi-header">
23
23
  <div>
24
- <div class="table-designer-sidebar__eyebrow">Table Designer</div>
25
- <div class="table-designer-sidebar__meta">
24
+ <div class="subnavi-header-title">Table Designer</div>
25
+ <div class="subnavi-header-details">
26
26
  ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? '' : 's'}
27
27
  </div>
28
28
  </div>
29
- <div class="table-designer-sidebar__header-actions">
30
- <button
31
- class="standard-button"
32
- data-action="import-table-designer-csv"
33
- type="button"
34
- >
35
- Import CSV
36
- </button>
37
- <input
38
- accept=".csv,text/csv"
39
- class="table-designer-sidebar__file-input"
40
- data-bind="table-designer-import-file"
41
- type="file"
42
- />
43
- <button
44
- class="signature-button"
45
- data-action="navigate"
46
- data-to="/table-designer/new"
47
- type="button"
48
- >
49
- + New Table
50
- </button>
51
-
52
- </div>
53
29
  </div>
54
30
 
55
31
  <label class="table-designer-sidebar__search">
@@ -64,7 +40,7 @@ export function renderTableDesignerSidebar(state) {
64
40
  />
65
41
  </label>
66
42
 
67
- <div class="table-designer-sidebar__list custom-scrollbar">
43
+ <div class="table-designer-sidebar__list subnavi-list custom-scrollbar">
68
44
  ${
69
45
  state.tableDesigner.loading && !tables.length
70
46
  ? `
@@ -76,7 +52,7 @@ export function renderTableDesignerSidebar(state) {
76
52
  : isNewDraft
77
53
  ? `
78
54
  <button
79
- class="table-designer-sidebar__item is-active w-full border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
55
+ class="table-designer-sidebar__item subnavi-item is-active border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
80
56
  data-action="navigate"
81
57
  data-to="/table-designer/new"
82
58
  type="button"
@@ -102,7 +78,7 @@ export function renderTableDesignerSidebar(state) {
102
78
  .map(
103
79
  table => `
104
80
  <button
105
- class="table-designer-sidebar__item w-full border px-4 py-3 text-left transition-colors ${
81
+ class="table-designer-sidebar__item subnavi-item border px-4 py-3 text-left transition-colors ${
106
82
  !isNewDraft && table.name === state.tableDesigner.selectedTableName
107
83
  ? 'is-active border-primary-container/30 bg-surface-container-high'
108
84
  : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
@@ -23,7 +23,8 @@ function renderPreviewHeader(draft, isVisible) {
23
23
  : ''
24
24
  }
25
25
  <button
26
- class="standard-button"
26
+ class="standard-button panel-toggle-button ${isVisible ? '' : 'is-active'}"
27
+ aria-pressed="${isVisible ? 'false' : 'true'}"
27
28
  data-action="toggle-table-designer-sql-preview"
28
29
  data-next-value="${isVisible ? 'false' : 'true'}"
29
30
  type="button"
@@ -49,10 +49,13 @@ const UI_PREFERENCE_STORAGE_KEYS = {
49
49
  dataTablesVisible: 'sqlite_hub_data_tables_visible',
50
50
  structureTablesVisible: 'sqlite_hub_structure_tables_visible',
51
51
  chartsHistoryVisible: 'sqlite_hub_charts_history_visible',
52
+ chartsQueryVisible: 'sqlite_hub_charts_query_visible',
52
53
  chartsResultsVisible: 'sqlite_hub_charts_results_visible',
54
+ tableDesignerTablesVisible: 'sqlite_hub_table_designer_tables_visible',
53
55
  tableDesignerSqlPreviewVisible: 'sqlite_hub_table_designer_sql_preview_visible',
54
56
  documentsEditorVisible: 'sqlite_hub_documents_editor_visible',
55
57
  documentsPreviewVisible: 'sqlite_hub_documents_preview_visible',
58
+ mediaTaggingViewerVisible: 'sqlite_hub_media_tagging_viewer_visible',
56
59
  };
57
60
  const QUERY_HISTORY_PAGE_SIZE = 30;
58
61
  const QUERY_HISTORY_RUN_LIMIT = 8;
@@ -63,6 +66,8 @@ const TEXT_EXPORT_FORMAT_LABELS = {
63
66
  csv: 'CSV',
64
67
  tsv: 'TSV',
65
68
  md: 'Markdown',
69
+ json: 'JSON',
70
+ parquet: 'Parquet',
66
71
  };
67
72
  const MISSING_DATABASE_ERROR = {
68
73
  code: 'ACTIVE_DATABASE_REQUIRED',
@@ -262,6 +267,9 @@ const state = {
262
267
  error: null,
263
268
  appVersion: null,
264
269
  sqliteVersion: null,
270
+ versionCheck: null,
271
+ versionCheckLoading: false,
272
+ versionCheckError: null,
265
273
  },
266
274
  overview: {
267
275
  data: null,
@@ -331,10 +339,13 @@ const state = {
331
339
  loading: false,
332
340
  error: null,
333
341
  historyTab: readStoredChartsHistoryTab(),
342
+ historySearchInput: '',
343
+ historySearch: '',
334
344
  historyPanelVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true),
345
+ detailPanelVisible: false,
335
346
  selectedHistoryId: null,
336
347
  chartHeightPreset: 'medium',
337
- sqlExpanded: false,
348
+ queryVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true),
338
349
  resultsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true),
339
350
  detail: null,
340
351
  detailLoading: false,
@@ -347,6 +358,7 @@ const state = {
347
358
  items: [],
348
359
  selectedId: null,
349
360
  selected: null,
361
+ searchQuery: '',
350
362
  draftFilename: '',
351
363
  draftContent: '',
352
364
  dirty: false,
@@ -363,6 +375,7 @@ const state = {
363
375
  tables: [],
364
376
  selectedTableName: null,
365
377
  draft: null,
378
+ tablesVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true),
366
379
  sqlPreviewVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true),
367
380
  pendingImportedDraft: null,
368
381
  loading: false,
@@ -415,7 +428,7 @@ const state = {
415
428
  issues: [],
416
429
  dismissedIssueKeys: [],
417
430
  selectedTagKeys: [],
418
- workflowMediaDetailsVisible: true,
431
+ workflowMediaDetailsVisible: readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible, true),
419
432
  workflowMediaRotationDegrees: 0,
420
433
  skippedMediaKeys: [],
421
434
  tagFormValues: {},
@@ -929,10 +942,13 @@ function resetChartsState() {
929
942
  state.charts.loading = false;
930
943
  state.charts.error = null;
931
944
  state.charts.historyTab = readStoredChartsHistoryTab(state.charts.historyTab);
945
+ state.charts.historySearchInput = '';
946
+ state.charts.historySearch = '';
932
947
  state.charts.historyPanelVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsHistoryVisible, true);
948
+ state.charts.detailPanelVisible = false;
933
949
  state.charts.selectedHistoryId = null;
934
950
  state.charts.chartHeightPreset = 'medium';
935
- state.charts.sqlExpanded = false;
951
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
936
952
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
937
953
  state.charts.detail = null;
938
954
  state.charts.detailLoading = false;
@@ -947,6 +963,7 @@ function resetDocumentsState() {
947
963
  state.documents.items = [];
948
964
  state.documents.selectedId = null;
949
965
  state.documents.selected = null;
966
+ state.documents.searchQuery = '';
950
967
  state.documents.draftFilename = '';
951
968
  state.documents.draftContent = '';
952
969
  state.documents.dirty = false;
@@ -1019,7 +1036,7 @@ function normalizeChartsHeightPreset(value) {
1019
1036
  }
1020
1037
 
1021
1038
  function normalizeChartsHistoryTab(value) {
1022
- return ['recent', 'saved'].includes(value) ? value : 'recent';
1039
+ return ['recent', 'saved', 'unsaved'].includes(value) ? value : 'recent';
1023
1040
  }
1024
1041
 
1025
1042
  function mergeQueryHistoryItemWithChartSummary(updatedItem, fallbackItem = null) {
@@ -1177,6 +1194,7 @@ function setMissingDatabaseState() {
1177
1194
  state.tableDesigner.tables = [];
1178
1195
  state.tableDesigner.selectedTableName = null;
1179
1196
  state.tableDesigner.draft = null;
1197
+ state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
1180
1198
  state.tableDesigner.sqlPreviewVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true);
1181
1199
  state.tableDesigner.pendingImportedDraft = null;
1182
1200
  state.tableDesigner.saving = false;
@@ -1358,6 +1376,33 @@ export function setSettingsSection(section) {
1358
1376
  emitChange();
1359
1377
  }
1360
1378
 
1379
+ export async function checkSettingsAppVersion() {
1380
+ state.settings.versionCheckLoading = true;
1381
+ state.settings.versionCheckError = null;
1382
+ emitChange();
1383
+
1384
+ try {
1385
+ const response = await api.checkAppVersion();
1386
+ const result = response.data ?? null;
1387
+
1388
+ state.settings.versionCheck = result;
1389
+ pushToast(
1390
+ result?.updateAvailable
1391
+ ? `SQLite Hub v${result.latestVersion} is available.`
1392
+ : 'SQLite Hub is up to date.',
1393
+ 'success',
1394
+ );
1395
+ return result;
1396
+ } catch (error) {
1397
+ state.settings.versionCheckError = normalizeError(error);
1398
+ pushToast('Version check failed.', 'alert');
1399
+ return null;
1400
+ } finally {
1401
+ state.settings.versionCheckLoading = false;
1402
+ emitChange();
1403
+ }
1404
+ }
1405
+
1361
1406
  export async function deleteSettingsApiToken(tokenId) {
1362
1407
  state.settings.tokenSaving = true;
1363
1408
  state.settings.error = null;
@@ -1557,7 +1602,8 @@ async function loadChartsDetail(historyId) {
1557
1602
 
1558
1603
  if (!Number.isInteger(numericId) || numericId < 1) {
1559
1604
  state.charts.selectedHistoryId = null;
1560
- state.charts.sqlExpanded = false;
1605
+ state.charts.detailPanelVisible = false;
1606
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
1561
1607
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
1562
1608
  state.charts.detail = null;
1563
1609
  state.charts.detailLoading = false;
@@ -1570,7 +1616,7 @@ async function loadChartsDetail(historyId) {
1570
1616
  }
1571
1617
 
1572
1618
  state.charts.selectedHistoryId = numericId;
1573
- state.charts.sqlExpanded = false;
1619
+ state.charts.queryVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, true);
1574
1620
  state.charts.resultsVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsResultsVisible, true);
1575
1621
  state.charts.detail = null;
1576
1622
  state.charts.detailLoading = true;
@@ -2344,6 +2390,7 @@ function invalidateDatabaseCaches(options = {}) {
2344
2390
  state.tableDesigner.tables = [];
2345
2391
  state.tableDesigner.selectedTableName = null;
2346
2392
  state.tableDesigner.draft = null;
2393
+ state.tableDesigner.tablesVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, true);
2347
2394
  state.tableDesigner.sqlPreviewVisible = readStoredBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerSqlPreviewVisible, true);
2348
2395
  state.tableDesigner.pendingImportedDraft = null;
2349
2396
  state.tableDesigner.saving = false;
@@ -2388,6 +2435,10 @@ function invalidateDatabaseCaches(options = {}) {
2388
2435
  state.mediaTagging.issues = [];
2389
2436
  state.mediaTagging.dismissedIssueKeys = [];
2390
2437
  state.mediaTagging.selectedTagKeys = [];
2438
+ state.mediaTagging.workflowMediaDetailsVisible = readStoredBoolean(
2439
+ UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible,
2440
+ true,
2441
+ );
2391
2442
  state.mediaTagging.workflowMediaRotationDegrees = 0;
2392
2443
  state.mediaTagging.skippedMediaKeys = [];
2393
2444
  state.mediaTagging.tagFormValues = {};
@@ -3448,7 +3499,8 @@ export async function openEditorRowUpdatePreview(rowIndex, values) {
3448
3499
  }
3449
3500
 
3450
3501
  export function toggleChartsSqlPanel() {
3451
- state.charts.sqlExpanded = !state.charts.sqlExpanded;
3502
+ state.charts.queryVisible = !state.charts.queryVisible;
3503
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.chartsQueryVisible, state.charts.queryVisible);
3452
3504
  emitChange();
3453
3505
  }
3454
3506
 
@@ -3470,6 +3522,17 @@ export function setChartsHistoryPanelVisibility(visible) {
3470
3522
  emitChange();
3471
3523
  }
3472
3524
 
3525
+ export function setChartsDetailPanelVisibility(visible) {
3526
+ const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.charts.detailPanelVisible);
3527
+
3528
+ if (state.charts.detailPanelVisible === nextValue) {
3529
+ return;
3530
+ }
3531
+
3532
+ state.charts.detailPanelVisible = nextValue;
3533
+ emitChange();
3534
+ }
3535
+
3473
3536
  export function setChartsHeightPreset(preset) {
3474
3537
  const nextPreset = normalizeChartsHeightPreset(preset);
3475
3538
 
@@ -3497,6 +3560,18 @@ export function setChartsHistoryTab(tab) {
3497
3560
  emitChange();
3498
3561
  }
3499
3562
 
3563
+ export function setChartsHistorySearchInput(query) {
3564
+ const nextQuery = String(query ?? '');
3565
+
3566
+ if (state.charts.historySearchInput === nextQuery && state.charts.historySearch === nextQuery.trim()) {
3567
+ return;
3568
+ }
3569
+
3570
+ state.charts.historySearchInput = nextQuery;
3571
+ state.charts.historySearch = nextQuery.trim();
3572
+ emitChange();
3573
+ }
3574
+
3500
3575
  export function updateCurrentQueryChartDraftField(field, value) {
3501
3576
  if (state.modal?.kind !== 'chart-editor' || !state.modal.draft) {
3502
3577
  return;
@@ -3935,7 +4010,9 @@ export function openQueryHistoryInEditor(historyId, options = {}) {
3935
4010
  clearQueryHistoryDetailState();
3936
4011
  state.editor.sqlText = options.append ? [state.editor.sqlText.trim(), rawSql].filter(Boolean).join('\n\n') : rawSql;
3937
4012
  storeString(UI_PREFERENCE_STORAGE_KEYS.sqlEditorQueryDraft, state.editor.sqlText);
3938
- emitChange();
4013
+ if (options.notify !== false) {
4014
+ emitChange();
4015
+ }
3939
4016
  return true;
3940
4017
  }
3941
4018
 
@@ -4042,6 +4119,12 @@ export function setTableDesignerSearchQuery(query) {
4042
4119
  emitChange();
4043
4120
  }
4044
4121
 
4122
+ export function toggleTableDesignerTablesPanel() {
4123
+ state.tableDesigner.tablesVisible = state.tableDesigner.tablesVisible === false;
4124
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.tableDesignerTablesVisible, state.tableDesigner.tablesVisible);
4125
+ emitChange();
4126
+ }
4127
+
4045
4128
  export function setDataTableSearchQuery(query) {
4046
4129
  state.dataBrowser.tableSearchQuery = String(query ?? '');
4047
4130
  emitChange();
@@ -4052,6 +4135,11 @@ export function setStructureTableSearchQuery(query) {
4052
4135
  emitChange();
4053
4136
  }
4054
4137
 
4138
+ export function setDocumentsSearchQuery(query) {
4139
+ state.documents.searchQuery = String(query ?? '');
4140
+ emitChange();
4141
+ }
4142
+
4055
4143
  export function setTableDesignerSqlPreviewVisibility(visible) {
4056
4144
  const nextValue = typeof visible === 'boolean' ? visible : !Boolean(state.tableDesigner.sqlPreviewVisible);
4057
4145
 
@@ -4293,6 +4381,7 @@ export function setMediaTaggingWorkflowMediaDetailsVisible(value, options = {})
4293
4381
  }
4294
4382
 
4295
4383
  state.mediaTagging.workflowMediaDetailsVisible = nextValue;
4384
+ storeBoolean(UI_PREFERENCE_STORAGE_KEYS.mediaTaggingViewerVisible, nextValue);
4296
4385
 
4297
4386
  if (options.notify !== false) {
4298
4387
  emitChange();
@@ -2,6 +2,8 @@ export const TEXT_EXPORT_EXTENSIONS = {
2
2
  csv: "csv",
3
3
  tsv: "tsv",
4
4
  md: "md",
5
+ json: "json",
6
+ parquet: "parquet",
5
7
  };
6
8
 
7
9
  export function normalizeTextExportFormat(format = "csv") {
@@ -10,7 +12,7 @@ export function normalizeTextExportFormat(format = "csv") {
10
12
  }
11
13
 
12
14
  function stripKnownTextExportExtension(value = "") {
13
- return String(value).replace(/\.(csv|tsv|md)$/i, "");
15
+ return String(value).replace(/\.(csv|tsv|md|json|parquet)$/i, "");
14
16
  }
15
17
 
16
18
  export function sanitizeExportFilenameBase(value, fallback = "export") {