juxscript 1.1.187 → 1.1.188
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 +158 -264
- package/lib/components/dataframe.ts +178 -291
- 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 (shared between fromUpload & withUpload)
|
|
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,93 @@ 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
|
+
// Row looks like headers if 70%+ non-numeric and not already the header row
|
|
545
|
+
if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
|
|
546
|
+
return i;
|
|
599
547
|
}
|
|
600
548
|
}
|
|
601
549
|
|
|
602
|
-
return
|
|
550
|
+
return 0;
|
|
603
551
|
}
|
|
604
552
|
|
|
605
|
-
|
|
606
|
-
*
|
|
607
|
-
*/
|
|
553
|
+
/* ═══════════════════════════════════════════════════
|
|
554
|
+
* RESHAPE MODAL
|
|
555
|
+
* ═══════════════════════════════════════════════════ */
|
|
556
|
+
|
|
608
557
|
private _showReshapeModal(): void {
|
|
609
558
|
if (!this._rawFileData) return;
|
|
610
559
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (isExcel) {
|
|
560
|
+
if (this._rawFileData.isExcel) {
|
|
614
561
|
this._showExcelReshapeModal();
|
|
615
562
|
} else {
|
|
616
563
|
this._showCSVReshapeModal();
|
|
617
564
|
}
|
|
618
565
|
}
|
|
619
566
|
|
|
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
|
|
567
|
+
private _cleanupReshapeModal(): void {
|
|
627
568
|
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
628
569
|
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
629
570
|
if (oldEl) oldEl.remove();
|
|
630
571
|
this._reshapeModal = null;
|
|
631
572
|
this._reshapeModalRendered = false;
|
|
632
573
|
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private async _showExcelReshapeModal(): Promise<void> {
|
|
577
|
+
if (!this._rawFileData?.file) return;
|
|
578
|
+
|
|
579
|
+
this._cleanupReshapeModal();
|
|
580
|
+
|
|
581
|
+
const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
|
|
633
582
|
|
|
634
|
-
// Create fresh modal
|
|
635
583
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
636
584
|
title: 'Excel Import Settings',
|
|
637
585
|
size: 'large',
|
|
@@ -639,37 +587,36 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
639
587
|
backdropClose: false
|
|
640
588
|
});
|
|
641
589
|
|
|
642
|
-
|
|
643
|
-
const modalContent = document.createElement('div');
|
|
644
|
-
modalContent.innerHTML = `
|
|
590
|
+
const modalContentHTML = `
|
|
645
591
|
<div style="margin-bottom: 1rem;">
|
|
646
592
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
647
593
|
Header Row (0-based)
|
|
648
594
|
</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%;"
|
|
595
|
+
<input
|
|
596
|
+
type="number"
|
|
597
|
+
id="${this._id}-header-row"
|
|
598
|
+
class="jux-input-element"
|
|
599
|
+
value="${suggestedRow}"
|
|
600
|
+
min="0"
|
|
601
|
+
max="50"
|
|
602
|
+
style="width: 100%;"
|
|
657
603
|
/>
|
|
658
|
-
<div
|
|
659
|
-
|
|
604
|
+
<div class="jux-reshape-hint">
|
|
605
|
+
<strong>Detected issue:</strong> The current header row appears to contain metadata or empty values.
|
|
606
|
+
Row ${suggestedRow} looks like it contains the actual column headers.
|
|
607
|
+
Adjust the value above and check the preview below.
|
|
660
608
|
</div>
|
|
661
609
|
</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;">
|
|
610
|
+
<div class="jux-reshape-preview-container">
|
|
664
611
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
665
612
|
Preview (first 10 rows)
|
|
666
613
|
</div>
|
|
667
|
-
<div id="${this._id}-preview"
|
|
614
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
668
615
|
</div>
|
|
669
616
|
`;
|
|
670
617
|
|
|
671
618
|
this._reshapeModal
|
|
672
|
-
.content(
|
|
619
|
+
.content(modalContentHTML)
|
|
673
620
|
.actions([
|
|
674
621
|
{
|
|
675
622
|
label: 'Cancel',
|
|
@@ -680,11 +627,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
680
627
|
label: 'Apply & Re-import',
|
|
681
628
|
variant: 'primary',
|
|
682
629
|
click: async () => {
|
|
683
|
-
const
|
|
684
|
-
const headerRow = parseInt(
|
|
630
|
+
const input = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
631
|
+
const headerRow = parseInt(input.value) || 0;
|
|
685
632
|
|
|
686
633
|
this.state.loading = true;
|
|
687
|
-
this._updateStatus('
|
|
634
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
688
635
|
|
|
689
636
|
try {
|
|
690
637
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
@@ -699,32 +646,28 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
699
646
|
if (sheetNames.length > 1) {
|
|
700
647
|
this._renderMultiSheet(sheets, this._rawFileData!.file.name);
|
|
701
648
|
} else {
|
|
702
|
-
this._showReshapeWarning = false;
|
|
649
|
+
this._showReshapeWarning = false;
|
|
703
650
|
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData!.file.name);
|
|
704
651
|
}
|
|
705
652
|
|
|
706
653
|
this._reshapeModal!.closeModal();
|
|
707
654
|
} catch (err: any) {
|
|
708
|
-
this._updateStatus(
|
|
655
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
709
656
|
}
|
|
710
657
|
}
|
|
711
658
|
}
|
|
712
659
|
]);
|
|
713
660
|
|
|
714
|
-
// Render modal to document.body and open it
|
|
715
661
|
this._reshapeModal.render(document.body);
|
|
716
662
|
this._reshapeModalRendered = true;
|
|
717
663
|
|
|
718
|
-
// Wait a tick for DOM to update after render
|
|
719
664
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
720
665
|
|
|
721
666
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
722
667
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
723
668
|
|
|
724
|
-
// Update preview on header row change
|
|
725
669
|
const updatePreview = async () => {
|
|
726
670
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
727
|
-
|
|
728
671
|
try {
|
|
729
672
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
730
673
|
headerRow,
|
|
@@ -733,18 +676,20 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
733
676
|
|
|
734
677
|
const firstSheet = Object.values(sheets)[0];
|
|
735
678
|
if (!firstSheet) {
|
|
736
|
-
if (previewDiv) previewDiv.textContent = '
|
|
679
|
+
if (previewDiv) previewDiv.textContent = 'No data found';
|
|
737
680
|
return;
|
|
738
681
|
}
|
|
739
682
|
|
|
740
683
|
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 ? '
|
|
684
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
685
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
743
686
|
}).join('\n');
|
|
744
687
|
|
|
745
|
-
if (previewDiv)
|
|
688
|
+
if (previewDiv) {
|
|
689
|
+
previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
690
|
+
}
|
|
746
691
|
} catch (err: any) {
|
|
747
|
-
if (previewDiv) previewDiv.textContent =
|
|
692
|
+
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
748
693
|
}
|
|
749
694
|
};
|
|
750
695
|
|
|
@@ -754,21 +699,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
754
699
|
this._reshapeModal.open();
|
|
755
700
|
}
|
|
756
701
|
|
|
757
|
-
/**
|
|
758
|
-
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
759
|
-
*/
|
|
760
702
|
private _showCSVReshapeModal(): void {
|
|
761
703
|
if (!this._rawFileData) return;
|
|
762
704
|
|
|
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
|
-
}
|
|
705
|
+
this._cleanupReshapeModal();
|
|
770
706
|
|
|
771
|
-
// Create fresh modal
|
|
772
707
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
773
708
|
title: 'CSV Import Settings',
|
|
774
709
|
size: 'large',
|
|
@@ -776,9 +711,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
776
711
|
backdropClose: false
|
|
777
712
|
});
|
|
778
713
|
|
|
779
|
-
|
|
780
|
-
const modalContent = document.createElement('div');
|
|
781
|
-
modalContent.innerHTML = `
|
|
714
|
+
const modalContentHTML = `
|
|
782
715
|
<div style="margin-bottom: 1rem;">
|
|
783
716
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
784
717
|
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
@@ -788,25 +721,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
788
721
|
<option value=";">Semicolon (;)</option>
|
|
789
722
|
</select>
|
|
790
723
|
</div>
|
|
791
|
-
|
|
792
724
|
<div style="margin-bottom: 1rem;">
|
|
793
725
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
794
726
|
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
795
727
|
</div>
|
|
796
|
-
|
|
797
728
|
<div style="margin-bottom: 1rem;">
|
|
798
729
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
799
730
|
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
800
731
|
</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;">
|
|
732
|
+
<div class="jux-reshape-preview-container">
|
|
803
733
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
804
|
-
<div id="${this._id}-preview"
|
|
734
|
+
<div id="${this._id}-preview" class="jux-reshape-preview"></div>
|
|
805
735
|
</div>
|
|
806
736
|
`;
|
|
807
737
|
|
|
808
738
|
this._reshapeModal
|
|
809
|
-
.content(
|
|
739
|
+
.content(modalContentHTML)
|
|
810
740
|
.actions([
|
|
811
741
|
{
|
|
812
742
|
label: 'Cancel',
|
|
@@ -828,7 +758,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
828
758
|
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
829
759
|
|
|
830
760
|
this.state.loading = true;
|
|
831
|
-
this._updateStatus('
|
|
761
|
+
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
832
762
|
|
|
833
763
|
try {
|
|
834
764
|
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
@@ -839,39 +769,35 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
839
769
|
});
|
|
840
770
|
|
|
841
771
|
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
842
|
-
this._showReshapeWarning = false;
|
|
772
|
+
this._showReshapeWarning = false;
|
|
843
773
|
this._setDataFrame(df, this._rawFileData.file.name);
|
|
844
774
|
|
|
845
775
|
this._reshapeModal!.closeModal();
|
|
846
776
|
} catch (err: any) {
|
|
847
|
-
this._updateStatus(
|
|
777
|
+
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
848
778
|
}
|
|
849
779
|
}
|
|
850
780
|
}
|
|
851
781
|
]);
|
|
852
782
|
|
|
853
|
-
// Render modal to document.body and open it
|
|
854
783
|
this._reshapeModal.render(document.body);
|
|
855
784
|
this._reshapeModalRendered = true;
|
|
856
785
|
|
|
857
|
-
// Use requestAnimationFrame to ensure DOM is ready
|
|
858
786
|
requestAnimationFrame(() => {
|
|
859
787
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
860
788
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
861
789
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
862
790
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
863
791
|
|
|
864
|
-
// Auto-detect initial values
|
|
865
792
|
if (this._rawFileData?.text) {
|
|
866
793
|
const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
|
|
867
794
|
if (delimiterSelect) delimiterSelect.value = detected === '\t' ? '\\t' : detected;
|
|
868
795
|
|
|
869
|
-
const
|
|
870
|
-
if (headerRowInput) headerRowInput.value = String(
|
|
796
|
+
const detectedHeaderRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
|
|
797
|
+
if (headerRowInput) headerRowInput.value = String(detectedHeaderRow);
|
|
871
798
|
}
|
|
872
799
|
|
|
873
|
-
|
|
874
|
-
const updatePreview = async () => {
|
|
800
|
+
const updatePreview = () => {
|
|
875
801
|
if (!this._rawFileData?.text) return;
|
|
876
802
|
|
|
877
803
|
const delim = delimiterSelect?.value === '\\t' ? '\t' : (delimiterSelect?.value || ',');
|
|
@@ -888,13 +814,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
888
814
|
});
|
|
889
815
|
|
|
890
816
|
const preview = df.toRows().map((row, i) => {
|
|
891
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
892
|
-
return `${i === 0 ? '
|
|
817
|
+
const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
|
|
818
|
+
return `${i === 0 ? '>> ' : ' '}${cols}`;
|
|
893
819
|
}).join('\n');
|
|
894
820
|
|
|
895
|
-
if (previewDiv)
|
|
821
|
+
if (previewDiv) {
|
|
822
|
+
previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
823
|
+
}
|
|
896
824
|
} catch (err: any) {
|
|
897
|
-
if (previewDiv) previewDiv.textContent =
|
|
825
|
+
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
898
826
|
}
|
|
899
827
|
};
|
|
900
828
|
|
|
@@ -903,17 +831,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
903
831
|
if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
|
|
904
832
|
|
|
905
833
|
updatePreview();
|
|
906
|
-
|
|
907
834
|
this._reshapeModal!.open();
|
|
908
835
|
});
|
|
909
836
|
}
|
|
910
837
|
|
|
911
|
-
update(_prop: string, _value: any): void { }
|
|
912
|
-
|
|
913
838
|
/* ═══════════════════════════════════════════════════
|
|
914
|
-
* RENDER
|
|
839
|
+
* UPDATE & RENDER
|
|
915
840
|
* ═══════════════════════════════════════════════════ */
|
|
916
841
|
|
|
842
|
+
update(_prop: string, _value: any): void { }
|
|
843
|
+
|
|
917
844
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
918
845
|
const container = this._setupContainer(targetId);
|
|
919
846
|
const { style, class: className } = this.state;
|
|
@@ -934,52 +861,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
934
861
|
}
|
|
935
862
|
|
|
936
863
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
937
|
-
|
|
938
864
|
this._uploadRef = upload;
|
|
939
|
-
|
|
865
|
+
|
|
940
866
|
this._pendingSource = async () => {
|
|
941
867
|
upload.bind('change', async (files: File[]) => {
|
|
942
868
|
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
|
-
}
|
|
869
|
+
await this._handleFile(files[0]);
|
|
983
870
|
});
|
|
984
871
|
};
|
|
985
872
|
|