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,9 +4,8 @@ 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
|
-
import { button } from './button.js'; // ✅ Import button factory
|
|
10
9
|
|
|
11
10
|
const TRIGGER_EVENTS = [] as const;
|
|
12
11
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'] as const;
|
|
@@ -22,10 +21,10 @@ export interface DataFrameOptions {
|
|
|
22
21
|
rowsPerPage?: number;
|
|
23
22
|
showStatus?: boolean;
|
|
24
23
|
icon?: string;
|
|
25
|
-
maxSheetSize?: number;
|
|
26
|
-
sheetChunkSize?: number;
|
|
27
|
-
maxFileSize?: number;
|
|
28
|
-
showReshapeWarning?: boolean;
|
|
24
|
+
maxSheetSize?: number;
|
|
25
|
+
sheetChunkSize?: number;
|
|
26
|
+
maxFileSize?: number;
|
|
27
|
+
showReshapeWarning?: boolean;
|
|
29
28
|
style?: string;
|
|
30
29
|
class?: string;
|
|
31
30
|
}
|
|
@@ -41,8 +40,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
41
40
|
private _df: DataFrame | null = null;
|
|
42
41
|
private _driver: TabularDriver;
|
|
43
42
|
private _table: Table | null = null;
|
|
44
|
-
private _tabs: Tabs | null = null;
|
|
45
|
-
private _sheets: Map<string, DataFrame> = new Map();
|
|
43
|
+
private _tabs: Tabs | null = null;
|
|
44
|
+
private _sheets: Map<string, DataFrame> = new Map();
|
|
46
45
|
private _tableOptions: {
|
|
47
46
|
striped: boolean;
|
|
48
47
|
hoverable: boolean;
|
|
@@ -51,19 +50,19 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
51
50
|
paginated: boolean;
|
|
52
51
|
rowsPerPage: number;
|
|
53
52
|
};
|
|
54
|
-
private _uploadRef: FileUpload | null = null;
|
|
53
|
+
private _uploadRef: FileUpload | null = null;
|
|
55
54
|
private _storageKey: string | null = null;
|
|
56
55
|
private _pendingSource: (() => Promise<void>) | null = null;
|
|
57
56
|
private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
|
|
58
57
|
private _showStatus: boolean = true;
|
|
59
58
|
private _icon: string = '';
|
|
60
|
-
private _maxSheetSize: number = 100000;
|
|
61
|
-
private _sheetChunkSize: number = 10000;
|
|
62
|
-
private _maxFileSize: number = 50;
|
|
59
|
+
private _maxSheetSize: number = 100000;
|
|
60
|
+
private _sheetChunkSize: number = 10000;
|
|
61
|
+
private _maxFileSize: number = 50;
|
|
63
62
|
private _showReshapeWarning: boolean = true;
|
|
64
63
|
private _rawFileData: { file: File; text?: string; isExcel?: boolean } | null = null;
|
|
65
|
-
private _reshapeModal: Modal | null = null;
|
|
66
|
-
private _reshapeModalRendered: boolean = false;
|
|
64
|
+
private _reshapeModal: Modal | null = null;
|
|
65
|
+
private _reshapeModalRendered: boolean = false;
|
|
67
66
|
|
|
68
67
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
69
68
|
super(id, {
|
|
@@ -111,7 +110,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
111
110
|
this._storageKey = key;
|
|
112
111
|
const loadFn = async () => {
|
|
113
112
|
this.state.loading = true;
|
|
114
|
-
this._updateStatus('
|
|
113
|
+
this._updateStatus('Loading...', 'loading');
|
|
115
114
|
try {
|
|
116
115
|
const df = await this._driver.loadByName(key);
|
|
117
116
|
if (!df) {
|
|
@@ -124,7 +123,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
124
123
|
} catch (err: any) {
|
|
125
124
|
this._triggerCallback('error', err.message, null, this);
|
|
126
125
|
this.state.loading = false;
|
|
127
|
-
this._updateStatus(
|
|
126
|
+
this._updateStatus(err.message, 'error');
|
|
128
127
|
}
|
|
129
128
|
};
|
|
130
129
|
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
@@ -136,63 +135,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
136
135
|
this._pendingSource = async () => {
|
|
137
136
|
upload.bind('change', async (files: File[]) => {
|
|
138
137
|
if (!files || files.length === 0) return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// ✅ Check file size
|
|
142
|
-
const fileSizeMB = file.size / (1024 * 1024);
|
|
143
|
-
if (fileSizeMB > this._maxFileSize) {
|
|
144
|
-
this._updateStatus(`❌ File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.state.loading = true;
|
|
149
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
153
|
-
file.name.toLowerCase().endsWith('.xls');
|
|
154
|
-
|
|
155
|
-
if (isExcel) {
|
|
156
|
-
// ✅ Store raw file for reshape
|
|
157
|
-
this._rawFileData = { file, isExcel: true };
|
|
158
|
-
|
|
159
|
-
// ✅ Pass chunking options for large files
|
|
160
|
-
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
161
|
-
maxSheetSize: this._maxSheetSize,
|
|
162
|
-
sheetChunkSize: this._sheetChunkSize,
|
|
163
|
-
onProgress: (loaded, total) => {
|
|
164
|
-
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
165
|
-
this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const sheetNames = Object.keys(sheets);
|
|
170
|
-
|
|
171
|
-
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
172
|
-
|
|
173
|
-
if (sheetNames.length > 1) {
|
|
174
|
-
this._renderMultiSheet(sheets, file.name);
|
|
175
|
-
} else {
|
|
176
|
-
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// ✅ CSV/TSV: Store raw text for reshaping
|
|
180
|
-
const text = await file.text();
|
|
181
|
-
this._rawFileData = { file, text, isExcel: false };
|
|
182
|
-
|
|
183
|
-
const df = this._driver.parseCSV(text, {
|
|
184
|
-
autoDetectDelimiter: true,
|
|
185
|
-
hasHeader: true
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
await this._driver.store(file.name, df, { source: file.name });
|
|
189
|
-
this._setDataFrame(df, file.name);
|
|
190
|
-
}
|
|
191
|
-
} catch (err: any) {
|
|
192
|
-
this._triggerCallback('error', err.message, null, this);
|
|
193
|
-
this.state.loading = false;
|
|
194
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
195
|
-
}
|
|
138
|
+
await this._handleFile(files[0]);
|
|
196
139
|
});
|
|
197
140
|
};
|
|
198
141
|
return this;
|
|
@@ -201,14 +144,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
201
144
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this {
|
|
202
145
|
const loadFn = async () => {
|
|
203
146
|
this.state.loading = true;
|
|
204
|
-
this._updateStatus('
|
|
147
|
+
this._updateStatus('Loading data...', 'loading');
|
|
205
148
|
try {
|
|
206
149
|
const df = new DataFrame(data);
|
|
207
150
|
this._setDataFrame(df, 'inline data');
|
|
208
151
|
} catch (err: any) {
|
|
209
152
|
this._triggerCallback('error', err.message, null, this);
|
|
210
153
|
this.state.loading = false;
|
|
211
|
-
this._updateStatus(
|
|
154
|
+
this._updateStatus(err.message, 'error');
|
|
212
155
|
}
|
|
213
156
|
};
|
|
214
157
|
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
@@ -296,38 +239,71 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
296
239
|
filterable(v: boolean): this { this._tableOptions.filterable = v; return this; }
|
|
297
240
|
paginated(v: boolean): this { this._tableOptions.paginated = v; return this; }
|
|
298
241
|
rowsPerPage(v: number): this { this._tableOptions.rowsPerPage = v; return this; }
|
|
242
|
+
maxSheetSize(v: number): this { this._maxSheetSize = v; return this; }
|
|
243
|
+
sheetChunkSize(v: number): this { this._sheetChunkSize = v; return this; }
|
|
244
|
+
maxFileSize(mb: number): this { this._maxFileSize = mb; return this; }
|
|
299
245
|
|
|
300
|
-
|
|
301
|
-
*
|
|
302
|
-
*/
|
|
303
|
-
maxSheetSize(v: number): this {
|
|
304
|
-
this._maxSheetSize = v;
|
|
305
|
-
return this;
|
|
306
|
-
}
|
|
246
|
+
/* ═══════════════════════════════════════════════════
|
|
247
|
+
* FILE HANDLING
|
|
248
|
+
* ═══════════════════════════════════════════════════ */
|
|
307
249
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
250
|
+
private async _handleFile(file: File): Promise<void> {
|
|
251
|
+
const fileSizeMB = file.size / (1024 * 1024);
|
|
252
|
+
if (fileSizeMB > this._maxFileSize) {
|
|
253
|
+
this._updateStatus(`File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
315
256
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
257
|
+
this.state.loading = true;
|
|
258
|
+
this._updateStatus('Parsing ' + file.name + '...', 'loading');
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
262
|
+
file.name.toLowerCase().endsWith('.xls');
|
|
263
|
+
|
|
264
|
+
if (isExcel) {
|
|
265
|
+
this._rawFileData = { file, isExcel: true };
|
|
266
|
+
|
|
267
|
+
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
268
|
+
maxSheetSize: this._maxSheetSize,
|
|
269
|
+
sheetChunkSize: this._sheetChunkSize,
|
|
270
|
+
onProgress: (loaded, total) => {
|
|
271
|
+
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
272
|
+
this._updateStatus(`Parsing ${file.name}... ${pct}%`, 'loading');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const sheetNames = Object.keys(sheets);
|
|
277
|
+
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
278
|
+
|
|
279
|
+
if (sheetNames.length > 1) {
|
|
280
|
+
this._renderMultiSheet(sheets, file.name);
|
|
281
|
+
} else {
|
|
282
|
+
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const text = await file.text();
|
|
286
|
+
this._rawFileData = { file, text, isExcel: false };
|
|
287
|
+
|
|
288
|
+
const df = this._driver.parseCSV(text, {
|
|
289
|
+
autoDetectDelimiter: true,
|
|
290
|
+
hasHeader: true
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await this._driver.store(file.name, df, { source: file.name });
|
|
294
|
+
this._setDataFrame(df, file.name);
|
|
295
|
+
}
|
|
296
|
+
} catch (err: any) {
|
|
297
|
+
this._triggerCallback('error', err.message, null, this);
|
|
298
|
+
this.state.loading = false;
|
|
299
|
+
this._updateStatus('Error: ' + err.message, 'error');
|
|
300
|
+
}
|
|
322
301
|
}
|
|
323
302
|
|
|
324
303
|
/* ═══════════════════════════════════════════════════
|
|
325
304
|
* MULTI-SHEET RENDERING
|
|
326
305
|
* ═══════════════════════════════════════════════════ */
|
|
327
306
|
|
|
328
|
-
/**
|
|
329
|
-
* ✅ FIXED: Render multiple Excel sheets as tabs
|
|
330
|
-
*/
|
|
331
307
|
private _renderMultiSheet(sheets: Record<string, DataFrame>, sourceName: string): void {
|
|
332
308
|
this.state.loading = false;
|
|
333
309
|
this._sheets.clear();
|
|
@@ -335,42 +311,35 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
335
311
|
const wrapper = document.getElementById(this._id);
|
|
336
312
|
if (!wrapper) return;
|
|
337
313
|
|
|
338
|
-
// Clear existing table if any
|
|
339
314
|
const existingTable = wrapper.querySelector('.jux-table-wrapper');
|
|
340
315
|
if (existingTable) existingTable.remove();
|
|
341
316
|
|
|
342
|
-
// Store all sheets
|
|
343
317
|
Object.entries(sheets).forEach(([name, df]) => {
|
|
344
318
|
this._sheets.set(name, df);
|
|
345
319
|
});
|
|
346
320
|
|
|
347
321
|
const sheetNames = Object.keys(sheets);
|
|
348
322
|
|
|
349
|
-
// ✅ FIX: Create tabs with EMPTY content first (just placeholders)
|
|
350
323
|
const tabs = new Tabs(`${this._id}-tabs`, {
|
|
351
324
|
tabs: sheetNames.map(name => ({
|
|
352
325
|
id: name,
|
|
353
326
|
label: name,
|
|
354
|
-
content: ''
|
|
327
|
+
content: ''
|
|
355
328
|
})),
|
|
356
329
|
activeTab: sheetNames[0]
|
|
357
330
|
});
|
|
358
331
|
|
|
359
332
|
this._tabs = tabs;
|
|
360
333
|
|
|
361
|
-
// Render tabs container
|
|
362
334
|
const tabsContainer = document.createElement('div');
|
|
363
335
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
364
336
|
wrapper.appendChild(tabsContainer);
|
|
365
337
|
|
|
366
|
-
// ✅ Render tabs NOW (creates all tab panels in DOM)
|
|
367
338
|
tabs.render(tabsContainer);
|
|
368
339
|
|
|
369
|
-
// ✅ NOW populate each tab with its DataFrame table (panels exist now)
|
|
370
340
|
sheetNames.forEach(sheetName => {
|
|
371
341
|
const df = sheets[sheetName];
|
|
372
342
|
|
|
373
|
-
// Create table
|
|
374
343
|
const table = new Table(`${this._id}-table-${sheetName}`, {
|
|
375
344
|
striped: this._tableOptions.striped,
|
|
376
345
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -380,25 +349,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
380
349
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
381
350
|
});
|
|
382
351
|
|
|
383
|
-
|
|
384
|
-
const columnDefs = df.columns.map(col => ({
|
|
385
|
-
key: col,
|
|
386
|
-
label: col
|
|
387
|
-
}));
|
|
388
|
-
|
|
352
|
+
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
389
353
|
table.columns(columnDefs).rows(df.toRows());
|
|
390
354
|
|
|
391
|
-
// ✅ Find the tab panel (it exists now because tabs.render() was called)
|
|
392
355
|
const tabPanel = document.getElementById(`${this._id}-tabs-${sheetName}-panel`);
|
|
393
|
-
if (!tabPanel)
|
|
394
|
-
console.error(`Tab panel not found: ${this._id}-tabs-${sheetName}-panel`);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
356
|
+
if (!tabPanel) return;
|
|
397
357
|
|
|
398
|
-
// ✅ Render table directly into tab panel
|
|
399
358
|
table.render(tabPanel);
|
|
400
359
|
|
|
401
|
-
// Add filter if enabled
|
|
402
360
|
if (this._tableOptions.filterable) {
|
|
403
361
|
const filterContainer = document.createElement('div');
|
|
404
362
|
filterContainer.className = 'jux-dataframe-filter';
|
|
@@ -421,19 +379,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
421
379
|
|
|
422
380
|
input.addEventListener('input', () => {
|
|
423
381
|
const text = input.value.toLowerCase();
|
|
424
|
-
if (!text) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const filtered = df.filter((row) => {
|
|
429
|
-
return Object.values(row).some(v =>
|
|
382
|
+
if (!text) { table.rows(df.toRows()); return; }
|
|
383
|
+
const filtered = df.filter((row) =>
|
|
384
|
+
Object.values(row).some(v =>
|
|
430
385
|
v !== null && v !== undefined && String(v).toLowerCase().includes(text)
|
|
431
|
-
)
|
|
432
|
-
|
|
386
|
+
)
|
|
387
|
+
);
|
|
433
388
|
table.rows(filtered.toRows());
|
|
434
389
|
});
|
|
435
390
|
|
|
436
|
-
// ✅ Insert filter BEFORE the table wrapper (which exists now)
|
|
437
391
|
const tableWrapper = tabPanel.querySelector('.jux-table-wrapper');
|
|
438
392
|
if (tableWrapper) {
|
|
439
393
|
tabPanel.insertBefore(filterContainer, tableWrapper);
|
|
@@ -441,16 +395,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
441
395
|
}
|
|
442
396
|
});
|
|
443
397
|
|
|
444
|
-
// Update status
|
|
445
398
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
446
399
|
this._updateStatus(
|
|
447
400
|
`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`,
|
|
448
401
|
'success'
|
|
449
402
|
);
|
|
450
403
|
|
|
451
|
-
// Set first sheet as active DataFrame
|
|
452
404
|
this._df = sheets[sheetNames[0]];
|
|
453
|
-
|
|
454
405
|
this._triggerCallback('load', this._df, null, this);
|
|
455
406
|
}
|
|
456
407
|
|
|
@@ -494,31 +445,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
494
445
|
this._df = df.select(...cleanCols);
|
|
495
446
|
}
|
|
496
447
|
|
|
497
|
-
// Update the table with new data
|
|
498
448
|
if (this._table && this._df) {
|
|
499
|
-
const columnDefs = this._df.columns.map(col => ({
|
|
500
|
-
key: col,
|
|
501
|
-
label: col
|
|
502
|
-
}));
|
|
449
|
+
const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
|
|
503
450
|
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
504
451
|
}
|
|
505
452
|
|
|
506
|
-
// ✅ Detect malformed data
|
|
507
453
|
const isMalformed = this._detectMalformedData(this._df!);
|
|
508
454
|
|
|
509
|
-
// ✅ Show warning if malformed
|
|
510
455
|
if (isMalformed && this._showReshapeWarning && this._rawFileData) {
|
|
511
456
|
this._updateStatus(
|
|
512
|
-
|
|
457
|
+
`${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols (Data may be malformed — headers may be on wrong row)`,
|
|
513
458
|
'warning'
|
|
514
459
|
);
|
|
515
460
|
|
|
516
|
-
// Add Fix Import Settings button after a tick to ensure status DOM is ready
|
|
517
461
|
requestAnimationFrame(() => {
|
|
518
462
|
const statusEl = document.getElementById(`${this._id}-status`);
|
|
519
463
|
if (statusEl) {
|
|
520
464
|
const settingsBtn = document.createElement('button');
|
|
521
|
-
settingsBtn.textContent = '
|
|
465
|
+
settingsBtn.textContent = 'Fix Import Settings';
|
|
522
466
|
settingsBtn.className = 'jux-button jux-button-sm jux-button-warning';
|
|
523
467
|
settingsBtn.style.marginLeft = '0.5rem';
|
|
524
468
|
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
@@ -531,13 +475,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
531
475
|
'success'
|
|
532
476
|
);
|
|
533
477
|
|
|
534
|
-
// ✅ Still add Settings button for manual adjustment
|
|
535
478
|
if (this._showReshapeWarning && this._rawFileData) {
|
|
536
479
|
requestAnimationFrame(() => {
|
|
537
480
|
const statusEl = document.getElementById(`${this._id}-status`);
|
|
538
481
|
if (statusEl) {
|
|
539
482
|
const settingsBtn = document.createElement('button');
|
|
540
|
-
settingsBtn.textContent = '
|
|
483
|
+
settingsBtn.textContent = 'Settings';
|
|
541
484
|
settingsBtn.className = 'jux-button jux-button-sm jux-button-ghost';
|
|
542
485
|
settingsBtn.style.marginLeft = '0.5rem';
|
|
543
486
|
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
@@ -550,88 +493,94 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
550
493
|
this._triggerCallback('load', this._df, null, this);
|
|
551
494
|
}
|
|
552
495
|
|
|
553
|
-
|
|
554
|
-
*
|
|
555
|
-
*/
|
|
496
|
+
/* ═══════════════════════════════════════════════════
|
|
497
|
+
* MALFORMED DATA DETECTION
|
|
498
|
+
* ═══════════════════════════════════════════════════ */
|
|
499
|
+
|
|
556
500
|
private _detectMalformedData(df: DataFrame): boolean {
|
|
557
501
|
const columns = df.columns;
|
|
558
502
|
const rows = df.toRows();
|
|
559
503
|
|
|
560
|
-
// Check 1: Columns have generic names like "__EMPTY", "_1", "col_0"
|
|
561
504
|
const hasGenericColumns = columns.some(col =>
|
|
562
505
|
col.startsWith('__EMPTY') ||
|
|
563
|
-
col.
|
|
506
|
+
col.match(/^_\d+$/) ||
|
|
564
507
|
col.match(/^col_\d+$/)
|
|
565
508
|
);
|
|
566
|
-
|
|
567
509
|
if (hasGenericColumns) return true;
|
|
568
510
|
|
|
569
|
-
// Check 2: First row values look like metadata (e.g., "Exported On:", "12/4/2025")
|
|
570
511
|
if (rows.length > 0) {
|
|
571
512
|
const firstRow = rows[0];
|
|
572
513
|
const values = Object.values(firstRow);
|
|
514
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
515
|
+
|
|
516
|
+
if (nonEmpty.length < columns.length * 0.5) return true;
|
|
517
|
+
|
|
573
518
|
const hasMetadata = values.some(v =>
|
|
574
519
|
String(v).includes('Exported') ||
|
|
575
520
|
String(v).includes('Generated') ||
|
|
576
521
|
String(v).includes('Report')
|
|
577
522
|
);
|
|
578
|
-
|
|
579
523
|
if (hasMetadata) return true;
|
|
580
524
|
}
|
|
581
525
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private _detectLikelyHeaderRow(df: DataFrame): number {
|
|
530
|
+
const rows = df.toRows();
|
|
531
|
+
|
|
532
|
+
for (let i = 0; i < Math.min(rows.length, 10); i++) {
|
|
533
|
+
const row = rows[i];
|
|
534
|
+
const values = Object.values(row);
|
|
535
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
586
536
|
|
|
587
|
-
|
|
588
|
-
const values = Object.values(row);
|
|
589
|
-
const nonNumeric = values.filter(v => {
|
|
590
|
-
const str = String(v).trim();
|
|
591
|
-
return isNaN(Number(str)) && str !== '';
|
|
592
|
-
}).length;
|
|
537
|
+
if (nonEmpty.length < values.length * 0.5) continue;
|
|
593
538
|
|
|
594
|
-
|
|
595
|
-
|
|
539
|
+
const nonNumericCount = nonEmpty.filter(v => {
|
|
540
|
+
const str = String(v).trim();
|
|
541
|
+
return isNaN(Number(str)) && str !== '';
|
|
542
|
+
}).length;
|
|
596
543
|
|
|
597
|
-
if (
|
|
598
|
-
|
|
544
|
+
if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
|
|
545
|
+
// i is index in toRows() but row 0 of the file was consumed as header,
|
|
546
|
+
// so the actual file row index is i + 1
|
|
547
|
+
return i + 1;
|
|
599
548
|
}
|
|
600
549
|
}
|
|
601
550
|
|
|
602
|
-
return
|
|
551
|
+
return 0;
|
|
603
552
|
}
|
|
604
553
|
|
|
605
|
-
|
|
606
|
-
*
|
|
607
|
-
*/
|
|
554
|
+
/* ═══════════════════════════════════════════════════
|
|
555
|
+
* RESHAPE MODAL
|
|
556
|
+
* ═══════════════════════════════════════════════════ */
|
|
557
|
+
|
|
608
558
|
private _showReshapeModal(): void {
|
|
609
559
|
if (!this._rawFileData) return;
|
|
610
560
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (isExcel) {
|
|
561
|
+
if (this._rawFileData.isExcel) {
|
|
614
562
|
this._showExcelReshapeModal();
|
|
615
563
|
} else {
|
|
616
564
|
this._showCSVReshapeModal();
|
|
617
565
|
}
|
|
618
566
|
}
|
|
619
567
|
|
|
620
|
-
|
|
621
|
-
* ✅ UPDATED: Excel reshape modal using Modal component
|
|
622
|
-
*/
|
|
623
|
-
private async _showExcelReshapeModal(): Promise<void> {
|
|
624
|
-
if (!this._rawFileData?.file) return;
|
|
625
|
-
|
|
626
|
-
// Remove old modal from DOM if it was previously rendered
|
|
568
|
+
private _cleanupReshapeModal(): void {
|
|
627
569
|
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
628
570
|
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
629
571
|
if (oldEl) oldEl.remove();
|
|
630
572
|
this._reshapeModal = null;
|
|
631
573
|
this._reshapeModalRendered = false;
|
|
632
574
|
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private async _showExcelReshapeModal(): Promise<void> {
|
|
578
|
+
if (!this._rawFileData?.file) return;
|
|
579
|
+
|
|
580
|
+
this._cleanupReshapeModal();
|
|
581
|
+
|
|
582
|
+
const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
|
|
633
583
|
|
|
634
|
-
// Create fresh modal
|
|
635
584
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
636
585
|
title: 'Excel Import Settings',
|
|
637
586
|
size: 'large',
|
|
@@ -639,37 +588,36 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
639
588
|
backdropClose: false
|
|
640
589
|
});
|
|
641
590
|
|
|
642
|
-
|
|
643
|
-
const modalContent = document.createElement('div');
|
|
644
|
-
modalContent.innerHTML = `
|
|
591
|
+
const modalContentHTML = `
|
|
645
592
|
<div style="margin-bottom: 1rem;">
|
|
646
593
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
647
594
|
Header Row (0-based)
|
|
648
595
|
</label>
|
|
649
|
-
<input
|
|
650
|
-
type="number"
|
|
651
|
-
id="${this._id}-header-row"
|
|
652
|
-
class="jux-input-element"
|
|
653
|
-
value="
|
|
654
|
-
min="0"
|
|
655
|
-
max="50"
|
|
656
|
-
style="width: 100%;"
|
|
596
|
+
<input
|
|
597
|
+
type="number"
|
|
598
|
+
id="${this._id}-header-row"
|
|
599
|
+
class="jux-input-element"
|
|
600
|
+
value="${suggestedRow}"
|
|
601
|
+
min="0"
|
|
602
|
+
max="50"
|
|
603
|
+
style="width: 100%;"
|
|
657
604
|
/>
|
|
658
|
-
<div
|
|
659
|
-
|
|
605
|
+
<div class="jux-reshape-hint">
|
|
606
|
+
<strong>Detected issue:</strong> The current header row appears to contain metadata or empty values.
|
|
607
|
+
Row ${suggestedRow} looks like it contains the actual column headers.
|
|
608
|
+
Adjust the value above and check the preview below.
|
|
660
609
|
</div>
|
|
661
610
|
</div>
|
|
662
|
-
|
|
663
|
-
<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;">
|
|
611
|
+
<div class="jux-reshape-preview-container">
|
|
664
612
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
665
613
|
Preview (first 10 rows)
|
|
666
614
|
</div>
|
|
667
|
-
<div id="${this._id}-preview"
|
|
615
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
668
616
|
</div>
|
|
669
617
|
`;
|
|
670
618
|
|
|
671
619
|
this._reshapeModal
|
|
672
|
-
.content(
|
|
620
|
+
.content(modalContentHTML)
|
|
673
621
|
.actions([
|
|
674
622
|
{
|
|
675
623
|
label: 'Cancel',
|
|
@@ -680,11 +628,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
680
628
|
label: 'Apply & Re-import',
|
|
681
629
|
variant: 'primary',
|
|
682
630
|
click: async () => {
|
|
683
|
-
const
|
|
684
|
-
const headerRow = parseInt(
|
|
631
|
+
const input = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
632
|
+
const headerRow = parseInt(input.value) || 0;
|
|
685
633
|
|
|
686
634
|
this.state.loading = true;
|
|
687
|
-
this._updateStatus('
|
|
635
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
688
636
|
|
|
689
637
|
try {
|
|
690
638
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
@@ -699,52 +647,50 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
699
647
|
if (sheetNames.length > 1) {
|
|
700
648
|
this._renderMultiSheet(sheets, this._rawFileData!.file.name);
|
|
701
649
|
} else {
|
|
702
|
-
this._showReshapeWarning = false;
|
|
650
|
+
this._showReshapeWarning = false;
|
|
703
651
|
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData!.file.name);
|
|
704
652
|
}
|
|
705
653
|
|
|
706
654
|
this._reshapeModal!.closeModal();
|
|
707
655
|
} catch (err: any) {
|
|
708
|
-
this._updateStatus(
|
|
656
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
709
657
|
}
|
|
710
658
|
}
|
|
711
659
|
}
|
|
712
660
|
]);
|
|
713
661
|
|
|
714
|
-
// Render modal to document.body and open it
|
|
715
662
|
this._reshapeModal.render(document.body);
|
|
716
663
|
this._reshapeModalRendered = true;
|
|
717
664
|
|
|
718
|
-
// Wait a tick for DOM to update after render
|
|
719
665
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
720
666
|
|
|
721
667
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
722
668
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
723
669
|
|
|
724
|
-
// Update preview on header row change
|
|
725
670
|
const updatePreview = async () => {
|
|
726
671
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
727
|
-
|
|
728
672
|
try {
|
|
729
673
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
730
674
|
headerRow,
|
|
731
|
-
maxSheetSize:
|
|
675
|
+
maxSheetSize: headerRow + 20
|
|
732
676
|
});
|
|
733
677
|
|
|
734
678
|
const firstSheet = Object.values(sheets)[0];
|
|
735
679
|
if (!firstSheet) {
|
|
736
|
-
if (previewDiv) previewDiv.textContent = '
|
|
680
|
+
if (previewDiv) previewDiv.textContent = 'No data found';
|
|
737
681
|
return;
|
|
738
682
|
}
|
|
739
683
|
|
|
740
684
|
const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
|
|
741
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
742
|
-
return `${i === 0 ? '
|
|
685
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
686
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
743
687
|
}).join('\n');
|
|
744
688
|
|
|
745
|
-
if (previewDiv)
|
|
689
|
+
if (previewDiv) {
|
|
690
|
+
previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
691
|
+
}
|
|
746
692
|
} catch (err: any) {
|
|
747
|
-
if (previewDiv) previewDiv.textContent =
|
|
693
|
+
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
748
694
|
}
|
|
749
695
|
};
|
|
750
696
|
|
|
@@ -754,21 +700,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
754
700
|
this._reshapeModal.open();
|
|
755
701
|
}
|
|
756
702
|
|
|
757
|
-
/**
|
|
758
|
-
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
759
|
-
*/
|
|
760
703
|
private _showCSVReshapeModal(): void {
|
|
761
704
|
if (!this._rawFileData) return;
|
|
762
705
|
|
|
763
|
-
|
|
764
|
-
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
765
|
-
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
766
|
-
if (oldEl) oldEl.remove();
|
|
767
|
-
this._reshapeModal = null;
|
|
768
|
-
this._reshapeModalRendered = false;
|
|
769
|
-
}
|
|
706
|
+
this._cleanupReshapeModal();
|
|
770
707
|
|
|
771
|
-
// Create fresh modal
|
|
772
708
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
773
709
|
title: 'CSV Import Settings',
|
|
774
710
|
size: 'large',
|
|
@@ -776,37 +712,32 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
776
712
|
backdropClose: false
|
|
777
713
|
});
|
|
778
714
|
|
|
779
|
-
|
|
780
|
-
const modalContent = document.createElement('div');
|
|
781
|
-
modalContent.innerHTML = `
|
|
715
|
+
const modalContentHTML = `
|
|
782
716
|
<div style="margin-bottom: 1rem;">
|
|
783
717
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
784
718
|
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
785
719
|
<option value=",">Comma (,)</option>
|
|
786
720
|
<option value="|">Pipe (|)</option>
|
|
787
|
-
<option value="
|
|
721
|
+
<option value="	">Tab (\\t)</option>
|
|
788
722
|
<option value=";">Semicolon (;)</option>
|
|
789
723
|
</select>
|
|
790
724
|
</div>
|
|
791
|
-
|
|
792
725
|
<div style="margin-bottom: 1rem;">
|
|
793
726
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
794
727
|
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
795
728
|
</div>
|
|
796
|
-
|
|
797
729
|
<div style="margin-bottom: 1rem;">
|
|
798
730
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
799
731
|
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
800
732
|
</div>
|
|
801
|
-
|
|
802
|
-
<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;">
|
|
733
|
+
<div class="jux-reshape-preview-container">
|
|
803
734
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
804
|
-
<div id="${this._id}-preview"
|
|
735
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
805
736
|
</div>
|
|
806
737
|
`;
|
|
807
738
|
|
|
808
739
|
this._reshapeModal
|
|
809
|
-
.content(
|
|
740
|
+
.content(modalContentHTML)
|
|
810
741
|
.actions([
|
|
811
742
|
{
|
|
812
743
|
label: 'Cancel',
|
|
@@ -823,12 +754,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
823
754
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
824
755
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
825
756
|
|
|
826
|
-
const delim = delimiterSelect.value
|
|
757
|
+
const delim = delimiterSelect.value;
|
|
827
758
|
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
828
759
|
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
829
760
|
|
|
830
761
|
this.state.loading = true;
|
|
831
|
-
this._updateStatus('
|
|
762
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
832
763
|
|
|
833
764
|
try {
|
|
834
765
|
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
@@ -839,42 +770,38 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
839
770
|
});
|
|
840
771
|
|
|
841
772
|
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
842
|
-
this._showReshapeWarning = false;
|
|
773
|
+
this._showReshapeWarning = false;
|
|
843
774
|
this._setDataFrame(df, this._rawFileData.file.name);
|
|
844
775
|
|
|
845
776
|
this._reshapeModal!.closeModal();
|
|
846
777
|
} catch (err: any) {
|
|
847
|
-
this._updateStatus(
|
|
778
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
848
779
|
}
|
|
849
780
|
}
|
|
850
781
|
}
|
|
851
782
|
]);
|
|
852
783
|
|
|
853
|
-
// Render modal to document.body and open it
|
|
854
784
|
this._reshapeModal.render(document.body);
|
|
855
785
|
this._reshapeModalRendered = true;
|
|
856
786
|
|
|
857
|
-
// Use requestAnimationFrame to ensure DOM is ready
|
|
858
787
|
requestAnimationFrame(() => {
|
|
859
788
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
860
789
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
861
790
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
862
791
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
863
792
|
|
|
864
|
-
// Auto-detect initial values
|
|
865
793
|
if (this._rawFileData?.text) {
|
|
866
794
|
const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
|
|
867
|
-
if (delimiterSelect) delimiterSelect.value = detected
|
|
795
|
+
if (delimiterSelect) delimiterSelect.value = detected;
|
|
868
796
|
|
|
869
|
-
const
|
|
870
|
-
if (headerRowInput) headerRowInput.value = String(
|
|
797
|
+
const detectedHeaderRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
|
|
798
|
+
if (headerRowInput) headerRowInput.value = String(detectedHeaderRow);
|
|
871
799
|
}
|
|
872
800
|
|
|
873
|
-
|
|
874
|
-
const updatePreview = async () => {
|
|
801
|
+
const updatePreview = () => {
|
|
875
802
|
if (!this._rawFileData?.text) return;
|
|
876
803
|
|
|
877
|
-
const delim = delimiterSelect?.value
|
|
804
|
+
const delim = delimiterSelect?.value || ',';
|
|
878
805
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
879
806
|
const skipRows = parseInt(skipRowsInput?.value) || 0;
|
|
880
807
|
|
|
@@ -888,13 +815,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
888
815
|
});
|
|
889
816
|
|
|
890
817
|
const preview = df.toRows().map((row, i) => {
|
|
891
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
892
|
-
return `${i === 0 ? '
|
|
818
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
819
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
893
820
|
}).join('\n');
|
|
894
821
|
|
|
895
|
-
if (previewDiv)
|
|
822
|
+
if (previewDiv) {
|
|
823
|
+
previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
824
|
+
}
|
|
896
825
|
} catch (err: any) {
|
|
897
|
-
if (previewDiv) previewDiv.textContent =
|
|
826
|
+
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
898
827
|
}
|
|
899
828
|
};
|
|
900
829
|
|
|
@@ -903,17 +832,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
903
832
|
if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
|
|
904
833
|
|
|
905
834
|
updatePreview();
|
|
906
|
-
|
|
907
835
|
this._reshapeModal!.open();
|
|
908
836
|
});
|
|
909
837
|
}
|
|
910
838
|
|
|
911
|
-
update(_prop: string, _value: any): void { }
|
|
912
|
-
|
|
913
839
|
/* ═══════════════════════════════════════════════════
|
|
914
|
-
* RENDER
|
|
840
|
+
* UPDATE & RENDER
|
|
915
841
|
* ═══════════════════════════════════════════════════ */
|
|
916
842
|
|
|
843
|
+
update(_prop: string, _value: any): void { }
|
|
844
|
+
|
|
917
845
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
918
846
|
const container = this._setupContainer(targetId);
|
|
919
847
|
const { style, class: className } = this.state;
|
|
@@ -934,52 +862,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
934
862
|
}
|
|
935
863
|
|
|
936
864
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
937
|
-
|
|
938
865
|
this._uploadRef = upload;
|
|
939
|
-
|
|
866
|
+
|
|
940
867
|
this._pendingSource = async () => {
|
|
941
868
|
upload.bind('change', async (files: File[]) => {
|
|
942
869
|
if (!files || files.length === 0) return;
|
|
943
|
-
|
|
944
|
-
this.state.loading = true;
|
|
945
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
946
|
-
|
|
947
|
-
try {
|
|
948
|
-
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
949
|
-
file.name.toLowerCase().endsWith('.xls');
|
|
950
|
-
|
|
951
|
-
if (isExcel) {
|
|
952
|
-
// ✅ Store raw file for reshape
|
|
953
|
-
this._rawFileData = { file, isExcel: true };
|
|
954
|
-
|
|
955
|
-
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
956
|
-
maxSheetSize: this._maxSheetSize,
|
|
957
|
-
sheetChunkSize: this._sheetChunkSize,
|
|
958
|
-
onProgress: (loaded, total) => {
|
|
959
|
-
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
960
|
-
this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
const sheetNames = Object.keys(sheets);
|
|
965
|
-
|
|
966
|
-
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
967
|
-
|
|
968
|
-
if (sheetNames.length > 1) {
|
|
969
|
-
this._renderMultiSheet(sheets, file.name);
|
|
970
|
-
} else {
|
|
971
|
-
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
972
|
-
}
|
|
973
|
-
} else {
|
|
974
|
-
const df = await this._driver.streamFile(file);
|
|
975
|
-
await this._driver.store(file.name, df, { source: file.name });
|
|
976
|
-
this._setDataFrame(df, file.name);
|
|
977
|
-
}
|
|
978
|
-
} catch (err: any) {
|
|
979
|
-
this._triggerCallback('error', err.message, null, this);
|
|
980
|
-
this.state.loading = false;
|
|
981
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
982
|
-
}
|
|
870
|
+
await this._handleFile(files[0]);
|
|
983
871
|
});
|
|
984
872
|
};
|
|
985
873
|
|