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.
- package/README.md +12 -4
- package/frontend/js/api.js +4 -2
- package/frontend/js/app.js +632 -12
- package/frontend/js/components/modal.js +432 -3
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +101 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +227 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +0 -3
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/package.json +2 -3
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- package/fill.js +0 -526
package/frontend/js/store.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|