juxscript 1.1.187 → 1.1.189
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/lib/components/dataframe.d.ts +3 -24
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +164 -269
- package/lib/components/dataframe.ts +184 -296
- package/lib/storage/TabularDriver.d.ts.map +1 -1
- package/lib/storage/TabularDriver.js +10 -3
- package/lib/storage/TabularDriver.ts +11 -3
- package/lib/styles/shadcn.css +196 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { TabularDriver } from '../storage/TabularDriver.js';
|
|
|
4
4
|
import { FileUpload } from './fileupload.js';
|
|
5
5
|
import { Table } from './table.js';
|
|
6
6
|
import { Tabs } from './tabs.js';
|
|
7
|
-
import { Modal } from './modal.js';
|
|
7
|
+
import { Modal } from './modal.js';
|
|
8
8
|
import { renderIcon } from './icons.js';
|
|
9
9
|
const TRIGGER_EVENTS = [];
|
|
10
10
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
@@ -23,21 +23,21 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
23
23
|
});
|
|
24
24
|
this._df = null;
|
|
25
25
|
this._table = null;
|
|
26
|
-
this._tabs = null;
|
|
27
|
-
this._sheets = new Map();
|
|
28
|
-
this._uploadRef = null;
|
|
26
|
+
this._tabs = null;
|
|
27
|
+
this._sheets = new Map();
|
|
28
|
+
this._uploadRef = null;
|
|
29
29
|
this._storageKey = null;
|
|
30
30
|
this._pendingSource = null;
|
|
31
31
|
this._inlineUpload = null;
|
|
32
32
|
this._showStatus = true;
|
|
33
33
|
this._icon = '';
|
|
34
|
-
this._maxSheetSize = 100000;
|
|
35
|
-
this._sheetChunkSize = 10000;
|
|
36
|
-
this._maxFileSize = 50;
|
|
34
|
+
this._maxSheetSize = 100000;
|
|
35
|
+
this._sheetChunkSize = 10000;
|
|
36
|
+
this._maxFileSize = 50;
|
|
37
37
|
this._showReshapeWarning = true;
|
|
38
38
|
this._rawFileData = null;
|
|
39
|
-
this._reshapeModal = null;
|
|
40
|
-
this._reshapeModalRendered = false;
|
|
39
|
+
this._reshapeModal = null;
|
|
40
|
+
this._reshapeModalRendered = false;
|
|
41
41
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
42
42
|
this._showStatus = options.showStatus ?? true;
|
|
43
43
|
this._icon = options.icon ?? '';
|
|
@@ -63,7 +63,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
63
63
|
this._storageKey = key;
|
|
64
64
|
const loadFn = async () => {
|
|
65
65
|
this.state.loading = true;
|
|
66
|
-
this._updateStatus('
|
|
66
|
+
this._updateStatus('Loading...', 'loading');
|
|
67
67
|
try {
|
|
68
68
|
const df = await this._driver.loadByName(key);
|
|
69
69
|
if (!df) {
|
|
@@ -77,7 +77,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
77
77
|
catch (err) {
|
|
78
78
|
this._triggerCallback('error', err.message, null, this);
|
|
79
79
|
this.state.loading = false;
|
|
80
|
-
this._updateStatus(
|
|
80
|
+
this._updateStatus(err.message, 'error');
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
83
|
if (this._table) {
|
|
@@ -94,56 +94,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
94
94
|
upload.bind('change', async (files) => {
|
|
95
95
|
if (!files || files.length === 0)
|
|
96
96
|
return;
|
|
97
|
-
|
|
98
|
-
// ✅ Check file size
|
|
99
|
-
const fileSizeMB = file.size / (1024 * 1024);
|
|
100
|
-
if (fileSizeMB > this._maxFileSize) {
|
|
101
|
-
this._updateStatus(`❌ File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
this.state.loading = true;
|
|
105
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
106
|
-
try {
|
|
107
|
-
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
108
|
-
file.name.toLowerCase().endsWith('.xls');
|
|
109
|
-
if (isExcel) {
|
|
110
|
-
// ✅ Store raw file for reshape
|
|
111
|
-
this._rawFileData = { file, isExcel: true };
|
|
112
|
-
// ✅ Pass chunking options for large files
|
|
113
|
-
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
114
|
-
maxSheetSize: this._maxSheetSize,
|
|
115
|
-
sheetChunkSize: this._sheetChunkSize,
|
|
116
|
-
onProgress: (loaded, total) => {
|
|
117
|
-
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
118
|
-
this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
const sheetNames = Object.keys(sheets);
|
|
122
|
-
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
123
|
-
if (sheetNames.length > 1) {
|
|
124
|
-
this._renderMultiSheet(sheets, file.name);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
// ✅ CSV/TSV: Store raw text for reshaping
|
|
132
|
-
const text = await file.text();
|
|
133
|
-
this._rawFileData = { file, text, isExcel: false };
|
|
134
|
-
const df = this._driver.parseCSV(text, {
|
|
135
|
-
autoDetectDelimiter: true,
|
|
136
|
-
hasHeader: true
|
|
137
|
-
});
|
|
138
|
-
await this._driver.store(file.name, df, { source: file.name });
|
|
139
|
-
this._setDataFrame(df, file.name);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
this._triggerCallback('error', err.message, null, this);
|
|
144
|
-
this.state.loading = false;
|
|
145
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
146
|
-
}
|
|
97
|
+
await this._handleFile(files[0]);
|
|
147
98
|
});
|
|
148
99
|
};
|
|
149
100
|
return this;
|
|
@@ -151,7 +102,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
151
102
|
fromData(data) {
|
|
152
103
|
const loadFn = async () => {
|
|
153
104
|
this.state.loading = true;
|
|
154
|
-
this._updateStatus('
|
|
105
|
+
this._updateStatus('Loading data...', 'loading');
|
|
155
106
|
try {
|
|
156
107
|
const df = new DataFrame(data);
|
|
157
108
|
this._setDataFrame(df, 'inline data');
|
|
@@ -159,7 +110,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
159
110
|
catch (err) {
|
|
160
111
|
this._triggerCallback('error', err.message, null, this);
|
|
161
112
|
this.state.loading = false;
|
|
162
|
-
this._updateStatus(
|
|
113
|
+
this._updateStatus(err.message, 'error');
|
|
163
114
|
}
|
|
164
115
|
};
|
|
165
116
|
if (this._table) {
|
|
@@ -237,68 +188,90 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
237
188
|
filterable(v) { this._tableOptions.filterable = v; return this; }
|
|
238
189
|
paginated(v) { this._tableOptions.paginated = v; return this; }
|
|
239
190
|
rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
191
|
+
maxSheetSize(v) { this._maxSheetSize = v; return this; }
|
|
192
|
+
sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
|
|
193
|
+
maxFileSize(mb) { this._maxFileSize = mb; return this; }
|
|
194
|
+
/* ═══════════════════════════════════════════════════
|
|
195
|
+
* FILE HANDLING
|
|
196
|
+
* ═══════════════════════════════════════════════════ */
|
|
197
|
+
async _handleFile(file) {
|
|
198
|
+
const fileSizeMB = file.size / (1024 * 1024);
|
|
199
|
+
if (fileSizeMB > this._maxFileSize) {
|
|
200
|
+
this._updateStatus(`File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.state.loading = true;
|
|
204
|
+
this._updateStatus('Parsing ' + file.name + '...', 'loading');
|
|
205
|
+
try {
|
|
206
|
+
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
207
|
+
file.name.toLowerCase().endsWith('.xls');
|
|
208
|
+
if (isExcel) {
|
|
209
|
+
this._rawFileData = { file, isExcel: true };
|
|
210
|
+
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
211
|
+
maxSheetSize: this._maxSheetSize,
|
|
212
|
+
sheetChunkSize: this._sheetChunkSize,
|
|
213
|
+
onProgress: (loaded, total) => {
|
|
214
|
+
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
215
|
+
this._updateStatus(`Parsing ${file.name}... ${pct}%`, 'loading');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const sheetNames = Object.keys(sheets);
|
|
219
|
+
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
220
|
+
if (sheetNames.length > 1) {
|
|
221
|
+
this._renderMultiSheet(sheets, file.name);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const text = await file.text();
|
|
229
|
+
this._rawFileData = { file, text, isExcel: false };
|
|
230
|
+
const df = this._driver.parseCSV(text, {
|
|
231
|
+
autoDetectDelimiter: true,
|
|
232
|
+
hasHeader: true
|
|
233
|
+
});
|
|
234
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
235
|
+
this._setDataFrame(df, file.name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
this._triggerCallback('error', err.message, null, this);
|
|
240
|
+
this.state.loading = false;
|
|
241
|
+
this._updateStatus('Error: ' + err.message, 'error');
|
|
242
|
+
}
|
|
260
243
|
}
|
|
261
244
|
/* ═══════════════════════════════════════════════════
|
|
262
245
|
* MULTI-SHEET RENDERING
|
|
263
246
|
* ═══════════════════════════════════════════════════ */
|
|
264
|
-
/**
|
|
265
|
-
* ✅ FIXED: Render multiple Excel sheets as tabs
|
|
266
|
-
*/
|
|
267
247
|
_renderMultiSheet(sheets, sourceName) {
|
|
268
248
|
this.state.loading = false;
|
|
269
249
|
this._sheets.clear();
|
|
270
250
|
const wrapper = document.getElementById(this._id);
|
|
271
251
|
if (!wrapper)
|
|
272
252
|
return;
|
|
273
|
-
// Clear existing table if any
|
|
274
253
|
const existingTable = wrapper.querySelector('.jux-table-wrapper');
|
|
275
254
|
if (existingTable)
|
|
276
255
|
existingTable.remove();
|
|
277
|
-
// Store all sheets
|
|
278
256
|
Object.entries(sheets).forEach(([name, df]) => {
|
|
279
257
|
this._sheets.set(name, df);
|
|
280
258
|
});
|
|
281
259
|
const sheetNames = Object.keys(sheets);
|
|
282
|
-
// ✅ FIX: Create tabs with EMPTY content first (just placeholders)
|
|
283
260
|
const tabs = new Tabs(`${this._id}-tabs`, {
|
|
284
261
|
tabs: sheetNames.map(name => ({
|
|
285
262
|
id: name,
|
|
286
263
|
label: name,
|
|
287
|
-
content: ''
|
|
264
|
+
content: ''
|
|
288
265
|
})),
|
|
289
266
|
activeTab: sheetNames[0]
|
|
290
267
|
});
|
|
291
268
|
this._tabs = tabs;
|
|
292
|
-
// Render tabs container
|
|
293
269
|
const tabsContainer = document.createElement('div');
|
|
294
270
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
295
271
|
wrapper.appendChild(tabsContainer);
|
|
296
|
-
// ✅ Render tabs NOW (creates all tab panels in DOM)
|
|
297
272
|
tabs.render(tabsContainer);
|
|
298
|
-
// ✅ NOW populate each tab with its DataFrame table (panels exist now)
|
|
299
273
|
sheetNames.forEach(sheetName => {
|
|
300
274
|
const df = sheets[sheetName];
|
|
301
|
-
// Create table
|
|
302
275
|
const table = new Table(`${this._id}-table-${sheetName}`, {
|
|
303
276
|
striped: this._tableOptions.striped,
|
|
304
277
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -307,21 +280,12 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
307
280
|
paginated: this._tableOptions.paginated,
|
|
308
281
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
309
282
|
});
|
|
310
|
-
|
|
311
|
-
const columnDefs = df.columns.map(col => ({
|
|
312
|
-
key: col,
|
|
313
|
-
label: col
|
|
314
|
-
}));
|
|
283
|
+
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
315
284
|
table.columns(columnDefs).rows(df.toRows());
|
|
316
|
-
// ✅ Find the tab panel (it exists now because tabs.render() was called)
|
|
317
285
|
const tabPanel = document.getElementById(`${this._id}-tabs-${sheetName}-panel`);
|
|
318
|
-
if (!tabPanel)
|
|
319
|
-
console.error(`Tab panel not found: ${this._id}-tabs-${sheetName}-panel`);
|
|
286
|
+
if (!tabPanel)
|
|
320
287
|
return;
|
|
321
|
-
}
|
|
322
|
-
// ✅ Render table directly into tab panel
|
|
323
288
|
table.render(tabPanel);
|
|
324
|
-
// Add filter if enabled
|
|
325
289
|
if (this._tableOptions.filterable) {
|
|
326
290
|
const filterContainer = document.createElement('div');
|
|
327
291
|
filterContainer.className = 'jux-dataframe-filter';
|
|
@@ -343,22 +307,17 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
343
307
|
table.rows(df.toRows());
|
|
344
308
|
return;
|
|
345
309
|
}
|
|
346
|
-
const filtered = df.filter((row) =>
|
|
347
|
-
return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
|
|
348
|
-
});
|
|
310
|
+
const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
|
|
349
311
|
table.rows(filtered.toRows());
|
|
350
312
|
});
|
|
351
|
-
// ✅ Insert filter BEFORE the table wrapper (which exists now)
|
|
352
313
|
const tableWrapper = tabPanel.querySelector('.jux-table-wrapper');
|
|
353
314
|
if (tableWrapper) {
|
|
354
315
|
tabPanel.insertBefore(filterContainer, tableWrapper);
|
|
355
316
|
}
|
|
356
317
|
}
|
|
357
318
|
});
|
|
358
|
-
// Update status
|
|
359
319
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
360
320
|
this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
|
|
361
|
-
// Set first sheet as active DataFrame
|
|
362
321
|
this._df = sheets[sheetNames[0]];
|
|
363
322
|
this._triggerCallback('load', this._df, null, this);
|
|
364
323
|
}
|
|
@@ -396,25 +355,18 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
396
355
|
if (cleanCols.length < df.columns.length) {
|
|
397
356
|
this._df = df.select(...cleanCols);
|
|
398
357
|
}
|
|
399
|
-
// Update the table with new data
|
|
400
358
|
if (this._table && this._df) {
|
|
401
|
-
const columnDefs = this._df.columns.map(col => ({
|
|
402
|
-
key: col,
|
|
403
|
-
label: col
|
|
404
|
-
}));
|
|
359
|
+
const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
|
|
405
360
|
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
406
361
|
}
|
|
407
|
-
// ✅ Detect malformed data
|
|
408
362
|
const isMalformed = this._detectMalformedData(this._df);
|
|
409
|
-
// ✅ Show warning if malformed
|
|
410
363
|
if (isMalformed && this._showReshapeWarning && this._rawFileData) {
|
|
411
|
-
this._updateStatus(
|
|
412
|
-
// Add Fix Import Settings button after a tick to ensure status DOM is ready
|
|
364
|
+
this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols (Data may be malformed — headers may be on wrong row)`, 'warning');
|
|
413
365
|
requestAnimationFrame(() => {
|
|
414
366
|
const statusEl = document.getElementById(`${this._id}-status`);
|
|
415
367
|
if (statusEl) {
|
|
416
368
|
const settingsBtn = document.createElement('button');
|
|
417
|
-
settingsBtn.textContent = '
|
|
369
|
+
settingsBtn.textContent = 'Fix Import Settings';
|
|
418
370
|
settingsBtn.className = 'jux-button jux-button-sm jux-button-warning';
|
|
419
371
|
settingsBtn.style.marginLeft = '0.5rem';
|
|
420
372
|
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
@@ -424,13 +376,12 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
424
376
|
}
|
|
425
377
|
else {
|
|
426
378
|
this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
|
|
427
|
-
// ✅ Still add Settings button for manual adjustment
|
|
428
379
|
if (this._showReshapeWarning && this._rawFileData) {
|
|
429
380
|
requestAnimationFrame(() => {
|
|
430
381
|
const statusEl = document.getElementById(`${this._id}-status`);
|
|
431
382
|
if (statusEl) {
|
|
432
383
|
const settingsBtn = document.createElement('button');
|
|
433
|
-
settingsBtn.textContent = '
|
|
384
|
+
settingsBtn.textContent = 'Settings';
|
|
434
385
|
settingsBtn.className = 'jux-button jux-button-sm jux-button-ghost';
|
|
435
386
|
settingsBtn.style.marginLeft = '0.5rem';
|
|
436
387
|
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
@@ -441,67 +392,65 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
441
392
|
}
|
|
442
393
|
this._triggerCallback('load', this._df, null, this);
|
|
443
394
|
}
|
|
444
|
-
|
|
445
|
-
*
|
|
446
|
-
*/
|
|
395
|
+
/* ═══════════════════════════════════════════════════
|
|
396
|
+
* MALFORMED DATA DETECTION
|
|
397
|
+
* ═══════════════════════════════════════════════════ */
|
|
447
398
|
_detectMalformedData(df) {
|
|
448
399
|
const columns = df.columns;
|
|
449
400
|
const rows = df.toRows();
|
|
450
|
-
// Check 1: Columns have generic names like "__EMPTY", "_1", "col_0"
|
|
451
401
|
const hasGenericColumns = columns.some(col => col.startsWith('__EMPTY') ||
|
|
452
|
-
col.
|
|
402
|
+
col.match(/^_\d+$/) ||
|
|
453
403
|
col.match(/^col_\d+$/));
|
|
454
404
|
if (hasGenericColumns)
|
|
455
405
|
return true;
|
|
456
|
-
// Check 2: First row values look like metadata (e.g., "Exported On:", "12/4/2025")
|
|
457
406
|
if (rows.length > 0) {
|
|
458
407
|
const firstRow = rows[0];
|
|
459
408
|
const values = Object.values(firstRow);
|
|
409
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
410
|
+
if (nonEmpty.length < columns.length * 0.5)
|
|
411
|
+
return true;
|
|
460
412
|
const hasMetadata = values.some(v => String(v).includes('Exported') ||
|
|
461
413
|
String(v).includes('Generated') ||
|
|
462
414
|
String(v).includes('Report'));
|
|
463
415
|
if (hasMetadata)
|
|
464
416
|
return true;
|
|
465
417
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
_detectLikelyHeaderRow(df) {
|
|
421
|
+
const rows = df.toRows();
|
|
422
|
+
for (let i = 0; i < Math.min(rows.length, 10); i++) {
|
|
423
|
+
const row = rows[i];
|
|
424
|
+
const values = Object.values(row);
|
|
425
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
426
|
+
if (nonEmpty.length < values.length * 0.5)
|
|
427
|
+
continue;
|
|
428
|
+
const nonNumericCount = nonEmpty.filter(v => {
|
|
429
|
+
const str = String(v).trim();
|
|
430
|
+
return isNaN(Number(str)) && str !== '';
|
|
431
|
+
}).length;
|
|
432
|
+
if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
|
|
433
|
+
// i is index in toRows() but row 0 of the file was consumed as header,
|
|
434
|
+
// so the actual file row index is i + 1
|
|
435
|
+
return i + 1;
|
|
480
436
|
}
|
|
481
437
|
}
|
|
482
|
-
return
|
|
438
|
+
return 0;
|
|
483
439
|
}
|
|
484
|
-
|
|
485
|
-
*
|
|
486
|
-
*/
|
|
440
|
+
/* ═══════════════════════════════════════════════════
|
|
441
|
+
* RESHAPE MODAL
|
|
442
|
+
* ═══════════════════════════════════════════════════ */
|
|
487
443
|
_showReshapeModal() {
|
|
488
444
|
if (!this._rawFileData)
|
|
489
445
|
return;
|
|
490
|
-
|
|
491
|
-
if (isExcel) {
|
|
446
|
+
if (this._rawFileData.isExcel) {
|
|
492
447
|
this._showExcelReshapeModal();
|
|
493
448
|
}
|
|
494
449
|
else {
|
|
495
450
|
this._showCSVReshapeModal();
|
|
496
451
|
}
|
|
497
452
|
}
|
|
498
|
-
|
|
499
|
-
* ✅ UPDATED: Excel reshape modal using Modal component
|
|
500
|
-
*/
|
|
501
|
-
async _showExcelReshapeModal() {
|
|
502
|
-
if (!this._rawFileData?.file)
|
|
503
|
-
return;
|
|
504
|
-
// Remove old modal from DOM if it was previously rendered
|
|
453
|
+
_cleanupReshapeModal() {
|
|
505
454
|
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
506
455
|
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
507
456
|
if (oldEl)
|
|
@@ -509,43 +458,47 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
509
458
|
this._reshapeModal = null;
|
|
510
459
|
this._reshapeModalRendered = false;
|
|
511
460
|
}
|
|
512
|
-
|
|
461
|
+
}
|
|
462
|
+
async _showExcelReshapeModal() {
|
|
463
|
+
if (!this._rawFileData?.file)
|
|
464
|
+
return;
|
|
465
|
+
this._cleanupReshapeModal();
|
|
466
|
+
const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
|
|
513
467
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
514
468
|
title: 'Excel Import Settings',
|
|
515
469
|
size: 'large',
|
|
516
470
|
close: true,
|
|
517
471
|
backdropClose: false
|
|
518
472
|
});
|
|
519
|
-
|
|
520
|
-
const modalContent = document.createElement('div');
|
|
521
|
-
modalContent.innerHTML = `
|
|
473
|
+
const modalContentHTML = `
|
|
522
474
|
<div style="margin-bottom: 1rem;">
|
|
523
475
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
524
476
|
Header Row (0-based)
|
|
525
477
|
</label>
|
|
526
|
-
<input
|
|
527
|
-
type="number"
|
|
528
|
-
id="${this._id}-header-row"
|
|
529
|
-
class="jux-input-element"
|
|
530
|
-
value="
|
|
531
|
-
min="0"
|
|
532
|
-
max="50"
|
|
533
|
-
style="width: 100%;"
|
|
478
|
+
<input
|
|
479
|
+
type="number"
|
|
480
|
+
id="${this._id}-header-row"
|
|
481
|
+
class="jux-input-element"
|
|
482
|
+
value="${suggestedRow}"
|
|
483
|
+
min="0"
|
|
484
|
+
max="50"
|
|
485
|
+
style="width: 100%;"
|
|
534
486
|
/>
|
|
535
|
-
<div
|
|
536
|
-
|
|
487
|
+
<div class="jux-reshape-hint">
|
|
488
|
+
<strong>Detected issue:</strong> The current header row appears to contain metadata or empty values.
|
|
489
|
+
Row ${suggestedRow} looks like it contains the actual column headers.
|
|
490
|
+
Adjust the value above and check the preview below.
|
|
537
491
|
</div>
|
|
538
492
|
</div>
|
|
539
|
-
|
|
540
|
-
<div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 400px; overflow-y: auto;">
|
|
493
|
+
<div class="jux-reshape-preview-container">
|
|
541
494
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
542
495
|
Preview (first 10 rows)
|
|
543
496
|
</div>
|
|
544
|
-
<div id="${this._id}-preview"
|
|
497
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
545
498
|
</div>
|
|
546
499
|
`;
|
|
547
500
|
this._reshapeModal
|
|
548
|
-
.content(
|
|
501
|
+
.content(modalContentHTML)
|
|
549
502
|
.actions([
|
|
550
503
|
{
|
|
551
504
|
label: 'Cancel',
|
|
@@ -556,10 +509,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
556
509
|
label: 'Apply & Re-import',
|
|
557
510
|
variant: 'primary',
|
|
558
511
|
click: async () => {
|
|
559
|
-
const
|
|
560
|
-
const headerRow = parseInt(
|
|
512
|
+
const input = document.getElementById(`${this._id}-header-row`);
|
|
513
|
+
const headerRow = parseInt(input.value) || 0;
|
|
561
514
|
this.state.loading = true;
|
|
562
|
-
this._updateStatus('
|
|
515
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
563
516
|
try {
|
|
564
517
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
565
518
|
headerRow,
|
|
@@ -572,48 +525,46 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
572
525
|
this._renderMultiSheet(sheets, this._rawFileData.file.name);
|
|
573
526
|
}
|
|
574
527
|
else {
|
|
575
|
-
this._showReshapeWarning = false;
|
|
528
|
+
this._showReshapeWarning = false;
|
|
576
529
|
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
|
|
577
530
|
}
|
|
578
531
|
this._reshapeModal.closeModal();
|
|
579
532
|
}
|
|
580
533
|
catch (err) {
|
|
581
|
-
this._updateStatus(
|
|
534
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
582
535
|
}
|
|
583
536
|
}
|
|
584
537
|
}
|
|
585
538
|
]);
|
|
586
|
-
// Render modal to document.body and open it
|
|
587
539
|
this._reshapeModal.render(document.body);
|
|
588
540
|
this._reshapeModalRendered = true;
|
|
589
|
-
// Wait a tick for DOM to update after render
|
|
590
541
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
591
542
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
592
543
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
593
|
-
// Update preview on header row change
|
|
594
544
|
const updatePreview = async () => {
|
|
595
545
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
596
546
|
try {
|
|
597
547
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
598
548
|
headerRow,
|
|
599
|
-
maxSheetSize:
|
|
549
|
+
maxSheetSize: headerRow + 20
|
|
600
550
|
});
|
|
601
551
|
const firstSheet = Object.values(sheets)[0];
|
|
602
552
|
if (!firstSheet) {
|
|
603
553
|
if (previewDiv)
|
|
604
|
-
previewDiv.textContent = '
|
|
554
|
+
previewDiv.textContent = 'No data found';
|
|
605
555
|
return;
|
|
606
556
|
}
|
|
607
557
|
const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
|
|
608
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
609
|
-
return `${i === 0 ? '
|
|
558
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
559
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
610
560
|
}).join('\n');
|
|
611
|
-
if (previewDiv)
|
|
612
|
-
previewDiv.textContent = `Columns: ${firstSheet.columns.join('
|
|
561
|
+
if (previewDiv) {
|
|
562
|
+
previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
563
|
+
}
|
|
613
564
|
}
|
|
614
565
|
catch (err) {
|
|
615
566
|
if (previewDiv)
|
|
616
|
-
previewDiv.textContent =
|
|
567
|
+
previewDiv.textContent = `Error: ${err.message}`;
|
|
617
568
|
}
|
|
618
569
|
};
|
|
619
570
|
if (headerRowInput)
|
|
@@ -621,57 +572,41 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
621
572
|
updatePreview();
|
|
622
573
|
this._reshapeModal.open();
|
|
623
574
|
}
|
|
624
|
-
/**
|
|
625
|
-
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
626
|
-
*/
|
|
627
575
|
_showCSVReshapeModal() {
|
|
628
576
|
if (!this._rawFileData)
|
|
629
577
|
return;
|
|
630
|
-
|
|
631
|
-
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
632
|
-
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
633
|
-
if (oldEl)
|
|
634
|
-
oldEl.remove();
|
|
635
|
-
this._reshapeModal = null;
|
|
636
|
-
this._reshapeModalRendered = false;
|
|
637
|
-
}
|
|
638
|
-
// Create fresh modal
|
|
578
|
+
this._cleanupReshapeModal();
|
|
639
579
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
640
580
|
title: 'CSV Import Settings',
|
|
641
581
|
size: 'large',
|
|
642
582
|
close: true,
|
|
643
583
|
backdropClose: false
|
|
644
584
|
});
|
|
645
|
-
|
|
646
|
-
const modalContent = document.createElement('div');
|
|
647
|
-
modalContent.innerHTML = `
|
|
585
|
+
const modalContentHTML = `
|
|
648
586
|
<div style="margin-bottom: 1rem;">
|
|
649
587
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
650
588
|
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
651
589
|
<option value=",">Comma (,)</option>
|
|
652
590
|
<option value="|">Pipe (|)</option>
|
|
653
|
-
<option value="
|
|
591
|
+
<option value="	">Tab (\\t)</option>
|
|
654
592
|
<option value=";">Semicolon (;)</option>
|
|
655
593
|
</select>
|
|
656
594
|
</div>
|
|
657
|
-
|
|
658
595
|
<div style="margin-bottom: 1rem;">
|
|
659
596
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
660
597
|
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
661
598
|
</div>
|
|
662
|
-
|
|
663
599
|
<div style="margin-bottom: 1rem;">
|
|
664
600
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
665
601
|
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
666
602
|
</div>
|
|
667
|
-
|
|
668
|
-
<div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 400px; overflow-y: auto;">
|
|
603
|
+
<div class="jux-reshape-preview-container">
|
|
669
604
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
670
|
-
<div id="${this._id}-preview"
|
|
605
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
671
606
|
</div>
|
|
672
607
|
`;
|
|
673
608
|
this._reshapeModal
|
|
674
|
-
.content(
|
|
609
|
+
.content(modalContentHTML)
|
|
675
610
|
.actions([
|
|
676
611
|
{
|
|
677
612
|
label: 'Cancel',
|
|
@@ -687,11 +622,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
687
622
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
688
623
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
689
624
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
690
|
-
const delim = delimiterSelect.value
|
|
625
|
+
const delim = delimiterSelect.value;
|
|
691
626
|
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
692
627
|
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
693
628
|
this.state.loading = true;
|
|
694
|
-
this._updateStatus('
|
|
629
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
695
630
|
try {
|
|
696
631
|
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
697
632
|
delimiter: delim,
|
|
@@ -700,39 +635,35 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
700
635
|
hasHeader: true
|
|
701
636
|
});
|
|
702
637
|
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
703
|
-
this._showReshapeWarning = false;
|
|
638
|
+
this._showReshapeWarning = false;
|
|
704
639
|
this._setDataFrame(df, this._rawFileData.file.name);
|
|
705
640
|
this._reshapeModal.closeModal();
|
|
706
641
|
}
|
|
707
642
|
catch (err) {
|
|
708
|
-
this._updateStatus(
|
|
643
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
709
644
|
}
|
|
710
645
|
}
|
|
711
646
|
}
|
|
712
647
|
]);
|
|
713
|
-
// Render modal to document.body and open it
|
|
714
648
|
this._reshapeModal.render(document.body);
|
|
715
649
|
this._reshapeModalRendered = true;
|
|
716
|
-
// Use requestAnimationFrame to ensure DOM is ready
|
|
717
650
|
requestAnimationFrame(() => {
|
|
718
651
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
719
652
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
720
653
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
721
654
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
722
|
-
// Auto-detect initial values
|
|
723
655
|
if (this._rawFileData?.text) {
|
|
724
656
|
const detected = this._driver._detectDelimiter(this._rawFileData.text);
|
|
725
657
|
if (delimiterSelect)
|
|
726
|
-
delimiterSelect.value = detected
|
|
727
|
-
const
|
|
658
|
+
delimiterSelect.value = detected;
|
|
659
|
+
const detectedHeaderRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
|
|
728
660
|
if (headerRowInput)
|
|
729
|
-
headerRowInput.value = String(
|
|
661
|
+
headerRowInput.value = String(detectedHeaderRow);
|
|
730
662
|
}
|
|
731
|
-
|
|
732
|
-
const updatePreview = async () => {
|
|
663
|
+
const updatePreview = () => {
|
|
733
664
|
if (!this._rawFileData?.text)
|
|
734
665
|
return;
|
|
735
|
-
const delim = delimiterSelect?.value
|
|
666
|
+
const delim = delimiterSelect?.value || ',';
|
|
736
667
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
737
668
|
const skipRows = parseInt(skipRowsInput?.value) || 0;
|
|
738
669
|
try {
|
|
@@ -744,15 +675,16 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
744
675
|
maxRows: 10
|
|
745
676
|
});
|
|
746
677
|
const preview = df.toRows().map((row, i) => {
|
|
747
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
748
|
-
return `${i === 0 ? '
|
|
678
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
679
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
749
680
|
}).join('\n');
|
|
750
|
-
if (previewDiv)
|
|
751
|
-
previewDiv.textContent = `Columns: ${df.columns.join('
|
|
681
|
+
if (previewDiv) {
|
|
682
|
+
previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
683
|
+
}
|
|
752
684
|
}
|
|
753
685
|
catch (err) {
|
|
754
686
|
if (previewDiv)
|
|
755
|
-
previewDiv.textContent =
|
|
687
|
+
previewDiv.textContent = `Error: ${err.message}`;
|
|
756
688
|
}
|
|
757
689
|
};
|
|
758
690
|
if (delimiterSelect)
|
|
@@ -765,10 +697,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
765
697
|
this._reshapeModal.open();
|
|
766
698
|
});
|
|
767
699
|
}
|
|
768
|
-
update(_prop, _value) { }
|
|
769
700
|
/* ═══════════════════════════════════════════════════
|
|
770
|
-
* RENDER
|
|
701
|
+
* UPDATE & RENDER
|
|
771
702
|
* ═══════════════════════════════════════════════════ */
|
|
703
|
+
update(_prop, _value) { }
|
|
772
704
|
render(targetId) {
|
|
773
705
|
const container = this._setupContainer(targetId);
|
|
774
706
|
const { style, class: className } = this.state;
|
|
@@ -789,48 +721,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
789
721
|
}
|
|
790
722
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
791
723
|
this._uploadRef = upload;
|
|
792
|
-
// ✅ FIX: Use the SAME logic as fromUpload() to handle multi-sheet
|
|
793
724
|
this._pendingSource = async () => {
|
|
794
725
|
upload.bind('change', async (files) => {
|
|
795
726
|
if (!files || files.length === 0)
|
|
796
727
|
return;
|
|
797
|
-
|
|
798
|
-
this.state.loading = true;
|
|
799
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
800
|
-
try {
|
|
801
|
-
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
802
|
-
file.name.toLowerCase().endsWith('.xls');
|
|
803
|
-
if (isExcel) {
|
|
804
|
-
// ✅ Store raw file for reshape
|
|
805
|
-
this._rawFileData = { file, isExcel: true };
|
|
806
|
-
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
807
|
-
maxSheetSize: this._maxSheetSize,
|
|
808
|
-
sheetChunkSize: this._sheetChunkSize,
|
|
809
|
-
onProgress: (loaded, total) => {
|
|
810
|
-
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
811
|
-
this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
const sheetNames = Object.keys(sheets);
|
|
815
|
-
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
816
|
-
if (sheetNames.length > 1) {
|
|
817
|
-
this._renderMultiSheet(sheets, file.name);
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
else {
|
|
824
|
-
const df = await this._driver.streamFile(file);
|
|
825
|
-
await this._driver.store(file.name, df, { source: file.name });
|
|
826
|
-
this._setDataFrame(df, file.name);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
catch (err) {
|
|
830
|
-
this._triggerCallback('error', err.message, null, this);
|
|
831
|
-
this.state.loading = false;
|
|
832
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
833
|
-
}
|
|
728
|
+
await this._handleFile(files[0]);
|
|
834
729
|
});
|
|
835
730
|
};
|
|
836
731
|
const uploadContainer = document.createElement('div');
|