sqlite-hub 0.12.0 → 0.16.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 (46) hide show
  1. package/README.md +12 -4
  2. package/frontend/js/api.js +4 -2
  3. package/frontend/js/app.js +632 -12
  4. package/frontend/js/components/modal.js +432 -3
  5. package/frontend/js/components/queryEditor.js +9 -1
  6. package/frontend/js/components/queryResults.js +79 -17
  7. package/frontend/js/components/rowEditorPanel.js +204 -10
  8. package/frontend/js/components/sidebar.js +101 -18
  9. package/frontend/js/components/tableDesignerEditor.js +69 -11
  10. package/frontend/js/store.js +227 -9
  11. package/frontend/js/utils/copyColumnExport.js +117 -0
  12. package/frontend/js/utils/exportFilenames.js +32 -0
  13. package/frontend/js/utils/filePathPreview.js +315 -0
  14. package/frontend/js/utils/format.js +1 -1
  15. package/frontend/js/utils/rowEditorJson.js +65 -0
  16. package/frontend/js/utils/sqlFormatter.js +691 -0
  17. package/frontend/js/utils/tableDesigner.js +178 -6
  18. package/frontend/js/utils/textCellStats.js +20 -0
  19. package/frontend/js/utils/timestampPreview.js +264 -0
  20. package/frontend/js/views/charts.js +3 -1
  21. package/frontend/js/views/data.js +34 -2
  22. package/frontend/js/views/editor.js +48 -1
  23. package/frontend/js/views/settings.js +0 -3
  24. package/frontend/js/views/structure.js +154 -212
  25. package/frontend/styles/base.css +6 -0
  26. package/frontend/styles/components.css +463 -2
  27. package/frontend/styles/structure-graph.css +0 -3
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +1 -1
  30. package/package.json +2 -3
  31. package/server/services/sqlite/introspection.js +10 -0
  32. package/server/services/sqlite/sqlExecutor.js +29 -0
  33. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  34. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  35. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  36. package/server/services/sqlite/tableDesignerService.js +1 -1
  37. package/tests/check-constraint-options.test.js +14 -0
  38. package/tests/export-filenames.test.js +34 -0
  39. package/tests/file-path-preview.test.js +165 -0
  40. package/tests/row-editor-json.test.js +82 -0
  41. package/tests/row-editor-timestamp-preview.test.js +192 -0
  42. package/tests/sql-formatter.test.js +173 -0
  43. package/tests/sql-highlight.test.js +38 -0
  44. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  45. package/tests/text-cell-stats.test.js +38 -0
  46. package/fill.js +0 -526
@@ -7,6 +7,7 @@ import {
7
7
  hydrateTableDesignerDraft,
8
8
  removeTableDesignerColumn,
9
9
  updateTableDesignerColumnField,
10
+ updateTableDesignerConstraintField,
10
11
  updateTableDesignerDraftField,
11
12
  } from './utils/tableDesigner.js';
12
13
  import {
@@ -22,6 +23,7 @@ import {
22
23
  MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
23
24
  MEDIA_TAGGING_DEFAULT_TAG_TABLE,
24
25
  } from './lib/mediaTaggingDefaults.js';
26
+ import { buildTextExportFilename } from './utils/exportFilenames.js';
25
27
 
26
28
  const listeners = new Set();
27
29
  const DEFAULT_SETTINGS = {
@@ -35,6 +37,9 @@ const DATA_FILTER_OPERATORS = new Set(['=', '!=', '<', '>', '<=', '>=', 'equals'
35
37
  const DATA_ROW_SIZE_STORAGE_KEY = 'data_row_size';
36
38
  const CHARTS_HISTORY_TAB_STORAGE_KEY = 'charts_history_tab';
37
39
  const QUERY_HISTORY_TAB_STORAGE_KEY = 'query_history_tab';
40
+ const COPY_COLUMN_SEPARATOR_STORAGE_KEY = 'sqlitehub.copyColumn.separator';
41
+ const COPY_COLUMN_WRAPPER_STORAGE_KEY = 'sqlitehub.copyColumn.wrapper';
42
+ const COPY_COLUMN_LINE_BREAKS_STORAGE_KEY = 'sqlitehub.copyColumn.lineBreaks';
38
43
  const UI_PREFERENCE_STORAGE_KEYS = {
39
44
  sqlEditorHistoryVisible: 'sqlite_hub_sql_editor_history_visible',
40
45
  sqlEditorEditorVisible: 'sqlite_hub_sql_editor_editor_visible',
@@ -50,6 +55,7 @@ const QUERY_HISTORY_PAGE_SIZE = 30;
50
55
  const QUERY_HISTORY_RUN_LIMIT = 8;
51
56
  const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
52
57
  const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
58
+ const COPY_COLUMN_MODES = new Set(['column', 'column-with-header', 'first-10', 'markdown-todo']);
53
59
  const TEXT_EXPORT_FORMAT_LABELS = {
54
60
  csv: 'CSV',
55
61
  tsv: 'TSV',
@@ -84,6 +90,34 @@ function storeDataPageSize(pageSize) {
84
90
  }
85
91
  }
86
92
 
93
+ function findQueryHistoryItemBySql(sql, snapshot = state) {
94
+ const normalizedSql = String(sql ?? '').trim();
95
+
96
+ if (!normalizedSql) {
97
+ return null;
98
+ }
99
+
100
+ if (String(snapshot.editor.historyDetail?.rawSql ?? '').trim() === normalizedSql) {
101
+ return snapshot.editor.historyDetail;
102
+ }
103
+
104
+ return snapshot.editor.history.find(entry => String(entry.rawSql ?? '').trim() === normalizedSql) ?? null;
105
+ }
106
+
107
+ function getCurrentQueryExportFilename(format = 'csv', filename = '') {
108
+ const queryText = String(state.editor.sqlText ?? '');
109
+ const historyItem = findQueryHistoryItemBySql(queryText);
110
+ const fallback = historyItem?.displayTitle || 'query-results';
111
+
112
+ return buildTextExportFilename(filename || fallback, { format, fallback });
113
+ }
114
+
115
+ function getCurrentDataTableExportFilename(format = 'csv', filename = '') {
116
+ const fallback = state.dataBrowser.selectedTable || 'table';
117
+
118
+ return buildTextExportFilename(filename || fallback, { format, fallback });
119
+ }
120
+
87
121
  function readStoredBoolean(key, fallback) {
88
122
  try {
89
123
  const value = globalThis.localStorage?.getItem(key);
@@ -127,6 +161,27 @@ function storeString(key, value) {
127
161
  }
128
162
  }
129
163
 
164
+ function readCopyColumnPreferences() {
165
+ return {
166
+ separator: readStoredString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, ','),
167
+ wrapper: readStoredString(COPY_COLUMN_WRAPPER_STORAGE_KEY, '"'),
168
+ lineBreaks: readStoredBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, false),
169
+ };
170
+ }
171
+
172
+ function normalizeCopyColumnMode(mode) {
173
+ const normalizedMode = String(mode ?? '').trim();
174
+ return COPY_COLUMN_MODES.has(normalizedMode) ? normalizedMode : 'column';
175
+ }
176
+
177
+ function normalizeCopyColumnScope(scope) {
178
+ return scope === 'charts' ? 'charts' : 'editor';
179
+ }
180
+
181
+ function getResultByCopyColumnScope(scope, snapshot = state) {
182
+ return normalizeCopyColumnScope(scope) === 'charts' ? snapshot.charts.result : snapshot.editor.result;
183
+ }
184
+
130
185
  function readStoredEditorActiveTab(fallback = 'messages') {
131
186
  try {
132
187
  const value = globalThis.localStorage?.getItem(UI_PREFERENCE_STORAGE_KEYS.sqlEditorActiveTab);
@@ -1480,6 +1535,16 @@ async function resolvePendingDataBrowserRow(version) {
1480
1535
  return;
1481
1536
  }
1482
1537
 
1538
+ if (!pendingTarget.identity && Number.isInteger(pendingTarget.rowIndex)) {
1539
+ if (table.rows?.[pendingTarget.rowIndex]) {
1540
+ state.dataBrowser.selectedRowIndex = pendingTarget.rowIndex;
1541
+ state.dataBrowser.selectedRow = null;
1542
+ }
1543
+
1544
+ state.dataBrowser.pendingOpenRow = null;
1545
+ return;
1546
+ }
1547
+
1483
1548
  const matchingRowIndex = findDataBrowserRowIndexByIdentity(table.rows ?? [], pendingTarget.identity);
1484
1549
 
1485
1550
  if (matchingRowIndex >= 0) {
@@ -2255,11 +2320,12 @@ export async function setRoute(route) {
2255
2320
  await loadRouteData(route);
2256
2321
  }
2257
2322
 
2258
- export function openModal(kind) {
2323
+ export function openModal(kind, options = {}) {
2259
2324
  state.modal = {
2260
2325
  kind,
2261
2326
  error: null,
2262
2327
  submitting: false,
2328
+ ...options,
2263
2329
  };
2264
2330
  emitChange();
2265
2331
  }
@@ -2272,6 +2338,7 @@ export function openQueryExportModal() {
2272
2338
 
2273
2339
  state.modal = {
2274
2340
  kind: 'query-export',
2341
+ filename: getCurrentQueryExportFilename('csv'),
2275
2342
  error: null,
2276
2343
  submitting: false,
2277
2344
  };
@@ -2286,12 +2353,98 @@ export function openDataExportModal() {
2286
2353
 
2287
2354
  state.modal = {
2288
2355
  kind: 'data-export',
2356
+ filename: getCurrentDataTableExportFilename('csv'),
2289
2357
  error: null,
2290
2358
  submitting: false,
2291
2359
  };
2292
2360
  emitChange();
2293
2361
  }
2294
2362
 
2363
+ export function openCopyColumnModal({ scope = 'editor', columnName = '', mode = 'column' } = {}) {
2364
+ const resultScope = normalizeCopyColumnScope(scope);
2365
+ const normalizedColumnName = String(columnName ?? '');
2366
+ const result = getResultByCopyColumnScope(resultScope);
2367
+ const hasColumn = (result?.columns ?? []).some(column => String(column) === normalizedColumnName);
2368
+
2369
+ if (!hasColumn) {
2370
+ pushToast('Column could not be found in the current result set.', 'alert');
2371
+ return;
2372
+ }
2373
+
2374
+ const preferences = readCopyColumnPreferences();
2375
+
2376
+ state.modal = {
2377
+ kind: 'copy-column',
2378
+ scope: resultScope,
2379
+ columnName: normalizedColumnName,
2380
+ copyMode: normalizeCopyColumnMode(mode),
2381
+ separator: preferences.separator,
2382
+ wrapper: preferences.wrapper,
2383
+ lineBreaks: preferences.lineBreaks,
2384
+ error: null,
2385
+ submitting: false,
2386
+ };
2387
+ emitChange();
2388
+ }
2389
+
2390
+ export function storeCopyColumnPreferences({ separator = ',', wrapper = '"', lineBreaks = false } = {}) {
2391
+ storeString(COPY_COLUMN_SEPARATOR_STORAGE_KEY, separator);
2392
+ storeString(COPY_COLUMN_WRAPPER_STORAGE_KEY, wrapper);
2393
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, lineBreaks);
2394
+
2395
+ if (state.modal?.kind === 'copy-column') {
2396
+ state.modal.separator = String(separator ?? '');
2397
+ state.modal.wrapper = String(wrapper ?? '');
2398
+ state.modal.lineBreaks = Boolean(lineBreaks);
2399
+ }
2400
+ }
2401
+
2402
+ export function updateCopyColumnModalFormatField(field, value) {
2403
+ if (state.modal?.kind !== 'copy-column') {
2404
+ return;
2405
+ }
2406
+
2407
+ const normalizedField = String(field ?? '').trim();
2408
+
2409
+ if (normalizedField !== 'separator' && normalizedField !== 'wrapper' && normalizedField !== 'lineBreaks') {
2410
+ return;
2411
+ }
2412
+
2413
+ const normalizedValue = normalizedField === 'lineBreaks' ? Boolean(value) : String(value ?? '');
2414
+ state.modal[normalizedField] = normalizedValue;
2415
+ state.modal.error = null;
2416
+
2417
+ if (normalizedField === 'lineBreaks') {
2418
+ storeBoolean(COPY_COLUMN_LINE_BREAKS_STORAGE_KEY, normalizedValue);
2419
+ } else {
2420
+ storeString(
2421
+ normalizedField === 'separator' ? COPY_COLUMN_SEPARATOR_STORAGE_KEY : COPY_COLUMN_WRAPPER_STORAGE_KEY,
2422
+ normalizedValue,
2423
+ );
2424
+ }
2425
+ emitChange();
2426
+ }
2427
+
2428
+ export function setCopyColumnModalSubmitting(submitting) {
2429
+ if (state.modal?.kind !== 'copy-column') {
2430
+ return;
2431
+ }
2432
+
2433
+ state.modal.submitting = Boolean(submitting);
2434
+ if (submitting) {
2435
+ state.modal.error = null;
2436
+ }
2437
+ emitChange();
2438
+ }
2439
+
2440
+ export function setCopyColumnModalError(error) {
2441
+ if (state.modal?.kind !== 'copy-column') {
2442
+ return;
2443
+ }
2444
+
2445
+ withModalError(error);
2446
+ }
2447
+
2295
2448
  export function openEditConnectionModal(id) {
2296
2449
  const connection = state.connections.recent.find(entry => entry.id === id);
2297
2450
 
@@ -3182,6 +3335,26 @@ export function updateCurrentTableDesignerColumnField(columnId, field, value, op
3182
3335
  }
3183
3336
  }
3184
3337
 
3338
+ export function updateCurrentTableDesignerConstraintField(constraintKind, constraintId, field, value, options = {}) {
3339
+ if (!state.tableDesigner.draft) {
3340
+ return;
3341
+ }
3342
+
3343
+ state.tableDesigner.draft = updateTableDesignerConstraintField(
3344
+ state.tableDesigner.draft,
3345
+ constraintKind,
3346
+ constraintId,
3347
+ field,
3348
+ value,
3349
+ getTableDesignerContext(),
3350
+ );
3351
+ state.tableDesigner.saveError = null;
3352
+
3353
+ if (options.notify !== false) {
3354
+ emitChange();
3355
+ }
3356
+ }
3357
+
3185
3358
  export function addCurrentTableDesignerColumn() {
3186
3359
  if (!state.tableDesigner.draft) {
3187
3360
  return null;
@@ -3676,6 +3849,26 @@ export function openDataRowByIdentity(tableName, identity) {
3676
3849
  return true;
3677
3850
  }
3678
3851
 
3852
+ export function preserveCurrentDataRowSelectionForReload() {
3853
+ const tableName = state.dataBrowser.selectedTable ?? state.dataBrowser.table?.name ?? '';
3854
+ const row = getSelectedDataBrowserRow();
3855
+ const rowIndex =
3856
+ typeof state.dataBrowser.selectedRowIndex === 'number' ? state.dataBrowser.selectedRowIndex : null;
3857
+
3858
+ if (!tableName || !row) {
3859
+ return false;
3860
+ }
3861
+
3862
+ state.dataBrowser.pendingOpenRow = {
3863
+ tableName,
3864
+ identity: row.__identity ?? null,
3865
+ rowIndex,
3866
+ };
3867
+ clearDataBrowserRowSelectionState();
3868
+ state.dataBrowser.saveError = null;
3869
+ return true;
3870
+ }
3871
+
3679
3872
  export function selectEditorRow(index) {
3680
3873
  const numericIndex = Number(index);
3681
3874
 
@@ -4208,14 +4401,19 @@ function finishCurrentQueryExport() {
4208
4401
  emitChange();
4209
4402
  }
4210
4403
 
4211
- export async function exportCurrentQueryFormat(format = 'csv') {
4404
+ export async function exportCurrentQueryFormat(format = 'csv', filename = '') {
4212
4405
  const normalizedFormat = String(format ?? 'csv').toLowerCase();
4213
4406
  const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
4407
+ const exportFilename = getCurrentQueryExportFilename(normalizedFormat, filename || state.modal?.filename);
4408
+
4409
+ if (state.modal?.kind === 'query-export') {
4410
+ state.modal.filename = exportFilename;
4411
+ }
4214
4412
 
4215
4413
  beginCurrentQueryExport();
4216
4414
 
4217
4415
  try {
4218
- await api.downloadQueryExport(state.editor.sqlText, normalizedFormat);
4416
+ await api.downloadQueryExport(state.editor.sqlText, normalizedFormat, { filename: exportFilename });
4219
4417
  closeModalInternal();
4220
4418
  pushToast(`${label} export started.`, 'success');
4221
4419
  return true;
@@ -4227,14 +4425,20 @@ export async function exportCurrentQueryFormat(format = 'csv') {
4227
4425
  }
4228
4426
  }
4229
4427
 
4230
- export async function duplicateCurrentQueryAsTable() {
4428
+ export async function duplicateCurrentQueryAsTable(filename = '') {
4429
+ const exportFilename = getCurrentQueryExportFilename('csv', filename || state.modal?.filename);
4430
+
4431
+ if (state.modal?.kind === 'query-export') {
4432
+ state.modal.filename = exportFilename;
4433
+ }
4434
+
4231
4435
  beginCurrentQueryExport();
4232
4436
 
4233
4437
  try {
4234
4438
  const response = await api.getQueryExport(state.editor.sqlText, 'csv');
4235
4439
  const exportData = response?.data ?? {};
4236
4440
  const imported = queueTableDesignerCsvImport(
4237
- exportData.filename || 'query-results.csv',
4441
+ exportFilename || getCurrentQueryExportFilename('csv', exportData.filename),
4238
4442
  exportData.content || '',
4239
4443
  { throwOnError: true },
4240
4444
  );
@@ -4301,7 +4505,7 @@ function finishCurrentDataTableExport() {
4301
4505
  emitChange();
4302
4506
  }
4303
4507
 
4304
- export async function exportCurrentDataTableFormat(format = 'csv') {
4508
+ export async function exportCurrentDataTableFormat(format = 'csv', filename = '') {
4305
4509
  const tableName = state.dataBrowser.selectedTable;
4306
4510
 
4307
4511
  if (!tableName) {
@@ -4311,11 +4515,19 @@ export async function exportCurrentDataTableFormat(format = 'csv') {
4311
4515
 
4312
4516
  const normalizedFormat = String(format ?? 'csv').toLowerCase();
4313
4517
  const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
4518
+ const exportFilename = getCurrentDataTableExportFilename(normalizedFormat, filename || state.modal?.filename);
4519
+
4520
+ if (state.modal?.kind === 'data-export') {
4521
+ state.modal.filename = exportFilename;
4522
+ }
4314
4523
 
4315
4524
  beginCurrentDataTableExport();
4316
4525
 
4317
4526
  try {
4318
- await api.downloadTableExport(tableName, getCurrentDataTableExportOptions(normalizedFormat));
4527
+ await api.downloadTableExport(tableName, {
4528
+ ...getCurrentDataTableExportOptions(normalizedFormat),
4529
+ filename: exportFilename,
4530
+ });
4319
4531
  closeModalInternal();
4320
4532
  pushToast(`${label} export started for ${tableName}.`, 'success');
4321
4533
  return true;
@@ -4327,7 +4539,7 @@ export async function exportCurrentDataTableFormat(format = 'csv') {
4327
4539
  }
4328
4540
  }
4329
4541
 
4330
- export async function duplicateCurrentDataTableAsTable() {
4542
+ export async function duplicateCurrentDataTableAsTable(filename = '') {
4331
4543
  const tableName = state.dataBrowser.selectedTable;
4332
4544
 
4333
4545
  if (!tableName) {
@@ -4335,13 +4547,19 @@ export async function duplicateCurrentDataTableAsTable() {
4335
4547
  return null;
4336
4548
  }
4337
4549
 
4550
+ const exportFilename = getCurrentDataTableExportFilename('csv', filename || state.modal?.filename);
4551
+
4552
+ if (state.modal?.kind === 'data-export') {
4553
+ state.modal.filename = exportFilename;
4554
+ }
4555
+
4338
4556
  beginCurrentDataTableExport();
4339
4557
 
4340
4558
  try {
4341
4559
  const response = await api.getTableExport(tableName, getCurrentDataTableExportOptions('csv'));
4342
4560
  const exportData = response?.data ?? {};
4343
4561
  const imported = queueTableDesignerCsvImport(
4344
- exportData.filename || `${tableName}.csv`,
4562
+ exportFilename || getCurrentDataTableExportFilename('csv', exportData.filename),
4345
4563
  exportData.content || '',
4346
4564
  {
4347
4565
  throwOnError: true,
@@ -0,0 +1,117 @@
1
+ export const COPY_COLUMN_MODE_VALUES = ["column", "column-with-header", "first-10", "markdown-todo"];
2
+
3
+ export function normalizeCopyColumnMode(mode) {
4
+ const normalizedMode = String(mode ?? "").trim();
5
+ return COPY_COLUMN_MODE_VALUES.includes(normalizedMode) ? normalizedMode : "column";
6
+ }
7
+
8
+ export function isMarkdownTodoCopyColumnMode(mode) {
9
+ return normalizeCopyColumnMode(mode) === "markdown-todo";
10
+ }
11
+
12
+ export function getCopyColumnActionLabel(copyMode) {
13
+ const normalizedMode = normalizeCopyColumnMode(copyMode);
14
+
15
+ if (normalizedMode === "column-with-header") {
16
+ return "Copy column with header";
17
+ }
18
+
19
+ if (normalizedMode === "first-10") {
20
+ return "Copy first 10";
21
+ }
22
+
23
+ if (normalizedMode === "markdown-todo") {
24
+ return "Export as Markdown Todo";
25
+ }
26
+
27
+ return "Copy column";
28
+ }
29
+
30
+ export function getCopyColumnExportMetadata(copyMode) {
31
+ return isMarkdownTodoCopyColumnMode(copyMode)
32
+ ? {
33
+ extension: "md",
34
+ label: "Markdown",
35
+ mimeType: "text/markdown;charset=utf-8",
36
+ suffix: "markdown-todo",
37
+ }
38
+ : {
39
+ extension: "txt",
40
+ label: "TXT",
41
+ mimeType: "text/plain;charset=utf-8",
42
+ suffix: normalizeCopyColumnMode(copyMode).replaceAll("-", "_"),
43
+ };
44
+ }
45
+
46
+ function stringifyCopyColumnValue(value) {
47
+ return value === null || value === undefined ? "" : String(value);
48
+ }
49
+
50
+ function formatDelimitedCopyColumnValue(value, wrapper) {
51
+ const text = stringifyCopyColumnValue(value);
52
+ const normalizedWrapper = String(wrapper ?? "");
53
+
54
+ if (!normalizedWrapper) {
55
+ return text;
56
+ }
57
+
58
+ return `${normalizedWrapper}${text
59
+ .split(normalizedWrapper)
60
+ .join(`${normalizedWrapper}${normalizedWrapper}`)}${normalizedWrapper}`;
61
+ }
62
+
63
+ function formatMarkdownTodoValue(value) {
64
+ return stringifyCopyColumnValue(value).replace(/\r\n|\r|\n/g, " ");
65
+ }
66
+
67
+ function getCopyColumnSourceRows(result, copyMode) {
68
+ const rows = result?.rows ?? [];
69
+ return normalizeCopyColumnMode(copyMode) === "first-10" ? rows.slice(0, 10) : rows;
70
+ }
71
+
72
+ export function buildCopyColumnText({
73
+ result,
74
+ columnName,
75
+ copyMode = "column",
76
+ separator = ",",
77
+ wrapper = "",
78
+ } = {}) {
79
+ const normalizedMode = normalizeCopyColumnMode(copyMode);
80
+ const sourceRows = getCopyColumnSourceRows(result, normalizedMode);
81
+ const values = sourceRows.map((row) => row?.[columnName]);
82
+
83
+ if (normalizedMode === "markdown-todo") {
84
+ return {
85
+ text: values.map((value) => `- [ ] ${formatMarkdownTodoValue(value)}`).join("\n"),
86
+ valueCount: values.length,
87
+ };
88
+ }
89
+
90
+ const outputValues = normalizedMode === "column-with-header" ? [columnName, ...values] : values;
91
+
92
+ return {
93
+ text: outputValues.map((value) => formatDelimitedCopyColumnValue(value, wrapper)).join(separator),
94
+ valueCount: values.length,
95
+ };
96
+ }
97
+
98
+ export function buildCopyColumnPreviewText({
99
+ result,
100
+ columnName,
101
+ copyMode = "column",
102
+ separator = ",",
103
+ wrapper = "",
104
+ maxRows = 4,
105
+ } = {}) {
106
+ const limitedRows = {
107
+ rows: getCopyColumnSourceRows(result, copyMode).slice(0, Math.max(0, Number(maxRows) || 0)),
108
+ };
109
+
110
+ return buildCopyColumnText({
111
+ result: limitedRows,
112
+ columnName,
113
+ copyMode: normalizeCopyColumnMode(copyMode) === "first-10" ? "column" : copyMode,
114
+ separator,
115
+ wrapper,
116
+ }).text;
117
+ }
@@ -0,0 +1,32 @@
1
+ export const TEXT_EXPORT_EXTENSIONS = {
2
+ csv: "csv",
3
+ tsv: "tsv",
4
+ md: "md",
5
+ };
6
+
7
+ export function normalizeTextExportFormat(format = "csv") {
8
+ const normalized = String(format ?? "csv").toLowerCase();
9
+ return TEXT_EXPORT_EXTENSIONS[normalized] ? normalized : "csv";
10
+ }
11
+
12
+ function stripKnownTextExportExtension(value = "") {
13
+ return String(value).replace(/\.(csv|tsv|md)$/i, "");
14
+ }
15
+
16
+ export function sanitizeExportFilenameBase(value, fallback = "export") {
17
+ const sanitized = stripKnownTextExportExtension(value)
18
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, " ")
19
+ .replace(/\s+/g, " ")
20
+ .trim()
21
+ .replace(/^[. ]+|[. ]+$/g, "");
22
+
23
+ return (sanitized || fallback).slice(0, 120);
24
+ }
25
+
26
+ export function buildTextExportFilename(value, { format = "csv", fallback = "export" } = {}) {
27
+ const normalizedFormat = normalizeTextExportFormat(format);
28
+ const extension = TEXT_EXPORT_EXTENSIONS[normalizedFormat];
29
+ const base = sanitizeExportFilenameBase(value, fallback);
30
+
31
+ return `${base}.${extension}`;
32
+ }