juxscript 1.1.184 → 1.1.186
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 +15 -4
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +282 -151
- package/lib/components/dataframe.ts +320 -167
- package/lib/storage/TabularDriver.d.ts +1 -2
- package/lib/storage/TabularDriver.d.ts.map +1 -1
- package/lib/storage/TabularDriver.js +13 -48
- package/lib/storage/TabularDriver.ts +13 -54
- package/lib/styles/shadcn.css +17 -0
- package/package.json +1 -1
|
@@ -45,6 +45,7 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
45
45
|
private _maxFileSize;
|
|
46
46
|
private _showReshapeWarning;
|
|
47
47
|
private _rawFileData;
|
|
48
|
+
private _reshapeModal;
|
|
48
49
|
constructor(id: string, options?: DataFrameOptions);
|
|
49
50
|
protected getTriggerEvents(): readonly string[];
|
|
50
51
|
protected getCallbackEvents(): readonly string[];
|
|
@@ -95,13 +96,23 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
95
96
|
private _renderMultiSheet;
|
|
96
97
|
private _updateStatus;
|
|
97
98
|
private _setDataFrame;
|
|
98
|
-
private _updateTable;
|
|
99
|
-
private _showFilterInput;
|
|
100
99
|
/**
|
|
101
|
-
* ✅ NEW:
|
|
100
|
+
* ✅ NEW: Detect if data looks malformed
|
|
101
|
+
*/
|
|
102
|
+
private _detectMalformedData;
|
|
103
|
+
/**
|
|
104
|
+
* ✅ UPDATED: Show settings modal using Modal component
|
|
102
105
|
*/
|
|
103
106
|
private _showReshapeModal;
|
|
104
|
-
|
|
107
|
+
/**
|
|
108
|
+
* ✅ UPDATED: Excel reshape modal using Modal component
|
|
109
|
+
*/
|
|
110
|
+
private _showExcelReshapeModal;
|
|
111
|
+
/**
|
|
112
|
+
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
113
|
+
*/
|
|
114
|
+
private _showCSVReshapeModal;
|
|
115
|
+
update(_prop: string, _value: any): void;
|
|
105
116
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
106
117
|
}
|
|
107
118
|
export declare function dataframe(id: string, options?: DataFrameOptions): DataFrameComponent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +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'; // ✅ Import Modal
|
|
7
8
|
import { renderIcon } from './icons.js';
|
|
8
9
|
const TRIGGER_EVENTS = [];
|
|
9
10
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
@@ -22,9 +23,9 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
22
23
|
});
|
|
23
24
|
this._df = null;
|
|
24
25
|
this._table = null;
|
|
25
|
-
this._tabs = null; // ✅ NEW: Tabs for multi-sheet Excel
|
|
26
|
+
this._tabs = null; // ✅ NEW: Tabs for multi-sheet Excel // @ts-ignore used for multi-sheet
|
|
26
27
|
this._sheets = new Map(); // ✅ NEW: Store all sheets
|
|
27
|
-
this._uploadRef = null;
|
|
28
|
+
this._uploadRef = null; // @ts-ignore used for reference tracking
|
|
28
29
|
this._storageKey = null;
|
|
29
30
|
this._pendingSource = null;
|
|
30
31
|
this._inlineUpload = null;
|
|
@@ -34,7 +35,8 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
34
35
|
this._sheetChunkSize = 10000; // ✅ Default 10k chunk
|
|
35
36
|
this._maxFileSize = 50; // ✅ Default 50MB
|
|
36
37
|
this._showReshapeWarning = true;
|
|
37
|
-
this._rawFileData = null;
|
|
38
|
+
this._rawFileData = null;
|
|
39
|
+
this._reshapeModal = null; // ✅ ADD THIS LINE
|
|
38
40
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
39
41
|
this._showStatus = options.showStatus ?? true;
|
|
40
42
|
this._icon = options.icon ?? '';
|
|
@@ -104,6 +106,8 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
104
106
|
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
105
107
|
file.name.toLowerCase().endsWith('.xls');
|
|
106
108
|
if (isExcel) {
|
|
109
|
+
// ✅ Store raw file for reshape
|
|
110
|
+
this._rawFileData = { file, isExcel: true };
|
|
107
111
|
// ✅ Pass chunking options for large files
|
|
108
112
|
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
109
113
|
maxSheetSize: this._maxSheetSize,
|
|
@@ -125,7 +129,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
125
129
|
else {
|
|
126
130
|
// ✅ CSV/TSV: Store raw text for reshaping
|
|
127
131
|
const text = await file.text();
|
|
128
|
-
this._rawFileData = { file, text };
|
|
132
|
+
this._rawFileData = { file, text, isExcel: false };
|
|
129
133
|
const df = this._driver.parseCSV(text, {
|
|
130
134
|
autoDetectDelimiter: true,
|
|
131
135
|
hasHeader: true
|
|
@@ -368,7 +372,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
368
372
|
if (type)
|
|
369
373
|
el.classList.add(`jux-dataframe-status-${type}`);
|
|
370
374
|
el.innerHTML = '';
|
|
371
|
-
if (this._icon && type === 'success') {
|
|
375
|
+
if (this._icon && (type === 'success' || type === 'warning')) {
|
|
372
376
|
const iconEl = renderIcon(this._icon);
|
|
373
377
|
iconEl.style.width = '16px';
|
|
374
378
|
iconEl.style.height = '16px';
|
|
@@ -391,155 +395,307 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
391
395
|
if (cleanCols.length < df.columns.length) {
|
|
392
396
|
this._df = df.select(...cleanCols);
|
|
393
397
|
}
|
|
394
|
-
|
|
395
|
-
this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
+
// Update the table with new data
|
|
399
|
+
if (this._table && this._df) {
|
|
400
|
+
const columnDefs = this._df.columns.map(col => ({
|
|
401
|
+
key: col,
|
|
402
|
+
label: col
|
|
403
|
+
}));
|
|
404
|
+
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
405
|
+
}
|
|
406
|
+
// ✅ Detect malformed data
|
|
407
|
+
const isMalformed = this._detectMalformedData(df);
|
|
408
|
+
// ✅ Show warning if malformed
|
|
409
|
+
if (isMalformed && this._showReshapeWarning && this._rawFileData) {
|
|
410
|
+
this._updateStatus(`⚠️ ${sourceName} — ${this._df.height} rows × ${this._df.width} cols (Data may be malformed)`, 'warning');
|
|
411
|
+
// Add Settings button
|
|
398
412
|
const statusEl = document.getElementById(`${this._id}-status`);
|
|
399
413
|
if (statusEl) {
|
|
400
414
|
const settingsBtn = document.createElement('button');
|
|
401
|
-
settingsBtn.textContent = 'Settings';
|
|
402
|
-
settingsBtn.className = 'jux-button jux-button-sm jux-button-
|
|
415
|
+
settingsBtn.textContent = 'Fix Import Settings';
|
|
416
|
+
settingsBtn.className = 'jux-button jux-button-sm jux-button-warning';
|
|
403
417
|
settingsBtn.style.marginLeft = '0.5rem';
|
|
404
418
|
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
405
419
|
statusEl.appendChild(settingsBtn);
|
|
406
420
|
}
|
|
407
421
|
}
|
|
422
|
+
else {
|
|
423
|
+
this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
|
|
424
|
+
// ✅ Still add Settings button for manual adjustment
|
|
425
|
+
if (this._showReshapeWarning && this._rawFileData) {
|
|
426
|
+
const statusEl = document.getElementById(`${this._id}-status`);
|
|
427
|
+
if (statusEl) {
|
|
428
|
+
const settingsBtn = document.createElement('button');
|
|
429
|
+
settingsBtn.textContent = 'Settings';
|
|
430
|
+
settingsBtn.className = 'jux-button jux-button-sm jux-button-ghost';
|
|
431
|
+
settingsBtn.style.marginLeft = '0.5rem';
|
|
432
|
+
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
433
|
+
statusEl.appendChild(settingsBtn);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
408
437
|
this._triggerCallback('load', this._df, null, this);
|
|
409
438
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
/**
|
|
440
|
+
* ✅ NEW: Detect if data looks malformed
|
|
441
|
+
*/
|
|
442
|
+
_detectMalformedData(df) {
|
|
443
|
+
const columns = df.columns;
|
|
444
|
+
const rows = df.toRows();
|
|
445
|
+
// Check 1: Columns have generic names like "__EMPTY", "_1", "col_0"
|
|
446
|
+
const hasGenericColumns = columns.some(col => col.startsWith('__EMPTY') ||
|
|
447
|
+
col.startsWith('_') ||
|
|
448
|
+
col.match(/^col_\d+$/));
|
|
449
|
+
if (hasGenericColumns)
|
|
450
|
+
return true;
|
|
451
|
+
// Check 2: First row values look like metadata (e.g., "Exported On:", "12/4/2025")
|
|
452
|
+
if (rows.length > 0) {
|
|
453
|
+
const firstRow = rows[0];
|
|
454
|
+
const values = Object.values(firstRow);
|
|
455
|
+
const hasMetadata = values.some(v => String(v).includes('Exported') ||
|
|
456
|
+
String(v).includes('Generated') ||
|
|
457
|
+
String(v).includes('Report'));
|
|
458
|
+
if (hasMetadata)
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
// Check 3: Row 2 or 3 has values that look like headers (mostly strings, no numbers)
|
|
462
|
+
if (rows.length >= 3) {
|
|
463
|
+
const secondRow = rows[1];
|
|
464
|
+
const thirdRow = rows[2];
|
|
465
|
+
const checkRow = (row) => {
|
|
466
|
+
const values = Object.values(row);
|
|
467
|
+
const nonNumeric = values.filter(v => {
|
|
468
|
+
const str = String(v).trim();
|
|
469
|
+
return isNaN(Number(str)) && str !== '';
|
|
470
|
+
}).length;
|
|
471
|
+
return nonNumeric >= values.length * 0.7; // 70% non-numeric
|
|
472
|
+
};
|
|
473
|
+
if (checkRow(secondRow) || checkRow(thirdRow)) {
|
|
474
|
+
return true;
|
|
440
475
|
}
|
|
441
476
|
}
|
|
477
|
+
return false;
|
|
442
478
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (
|
|
479
|
+
/**
|
|
480
|
+
* ✅ UPDATED: Show settings modal using Modal component
|
|
481
|
+
*/
|
|
482
|
+
_showReshapeModal() {
|
|
483
|
+
if (!this._rawFileData)
|
|
448
484
|
return;
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
input.type = 'text';
|
|
453
|
-
input.placeholder = 'Filter rows...';
|
|
454
|
-
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
455
|
-
const iconEl = renderIcon('search');
|
|
456
|
-
iconEl.style.width = '16px';
|
|
457
|
-
iconEl.style.height = '16px';
|
|
458
|
-
const iconWrap = document.createElement('span');
|
|
459
|
-
iconWrap.className = 'jux-dataframe-filter-icon';
|
|
460
|
-
iconWrap.appendChild(iconEl);
|
|
461
|
-
filterContainer.appendChild(iconWrap);
|
|
462
|
-
filterContainer.appendChild(input);
|
|
463
|
-
const tableElement = wrapper.querySelector('.jux-table-wrapper table');
|
|
464
|
-
if (tableElement && tableElement.parentElement) {
|
|
465
|
-
tableElement.parentElement.insertBefore(filterContainer, tableElement);
|
|
485
|
+
const isExcel = this._rawFileData.isExcel;
|
|
486
|
+
if (isExcel) {
|
|
487
|
+
this._showExcelReshapeModal();
|
|
466
488
|
}
|
|
467
489
|
else {
|
|
468
|
-
|
|
490
|
+
this._showCSVReshapeModal();
|
|
469
491
|
}
|
|
470
|
-
input.addEventListener('input', () => {
|
|
471
|
-
if (!this._df)
|
|
472
|
-
return;
|
|
473
|
-
const text = input.value.toLowerCase();
|
|
474
|
-
if (!text) {
|
|
475
|
-
this._table?.rows(this._df.toRows());
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const filtered = this._df.filter((row) => {
|
|
479
|
-
return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
|
|
480
|
-
});
|
|
481
|
-
this._table?.rows(filtered.toRows());
|
|
482
|
-
});
|
|
483
492
|
}
|
|
484
493
|
/**
|
|
485
|
-
* ✅
|
|
494
|
+
* ✅ UPDATED: Excel reshape modal using Modal component
|
|
486
495
|
*/
|
|
487
|
-
|
|
488
|
-
if (!this._rawFileData)
|
|
496
|
+
async _showExcelReshapeModal() {
|
|
497
|
+
if (!this._rawFileData?.file)
|
|
489
498
|
return;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
</div>
|
|
518
|
-
|
|
519
|
-
<div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 300px; overflow-y: auto;">
|
|
520
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
521
|
-
<div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
|
|
522
|
-
</div>
|
|
499
|
+
// Create modal if not exists
|
|
500
|
+
if (!this._reshapeModal) {
|
|
501
|
+
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
502
|
+
title: 'Excel Import Settings',
|
|
503
|
+
size: 'large',
|
|
504
|
+
close: true,
|
|
505
|
+
backdropClose: false
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// Build modal content
|
|
509
|
+
const modalContent = document.createElement('div');
|
|
510
|
+
modalContent.innerHTML = `
|
|
511
|
+
<div style="margin-bottom: 1rem;">
|
|
512
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
513
|
+
Header Row (0-based)
|
|
514
|
+
</label>
|
|
515
|
+
<input
|
|
516
|
+
type="number"
|
|
517
|
+
id="${this._id}-header-row"
|
|
518
|
+
class="jux-input-element"
|
|
519
|
+
value="0"
|
|
520
|
+
min="0"
|
|
521
|
+
max="50"
|
|
522
|
+
style="width: 100%;"
|
|
523
|
+
/>
|
|
524
|
+
<div style="font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem; padding: 0.5rem; background: hsl(var(--warning) / 0.1); border-radius: var(--radius); border: 1px solid hsl(var(--warning) / 0.3);">
|
|
525
|
+
⚠️ <strong>Detected issue:</strong> Row 0 contains metadata ("Exported On:"), row 2 contains actual headers ("340B ID"). Try setting this to <strong>2</strong>.
|
|
523
526
|
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<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;">
|
|
530
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
531
|
+
Preview (first 10 rows)
|
|
527
532
|
</div>
|
|
533
|
+
<div id="${this._id}-preview" style="font-family: 'JetBrains Mono', 'Courier New', monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--foreground)); line-height: 1.5;"></div>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
536
|
+
this._reshapeModal
|
|
537
|
+
.content(modalContent.innerHTML)
|
|
538
|
+
.actions([
|
|
539
|
+
{
|
|
540
|
+
label: 'Cancel',
|
|
541
|
+
variant: 'secondary',
|
|
542
|
+
click: () => this._reshapeModal.closeModal()
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
label: 'Apply & Re-import',
|
|
546
|
+
variant: 'primary',
|
|
547
|
+
click: async () => {
|
|
548
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
549
|
+
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
550
|
+
this.state.loading = true;
|
|
551
|
+
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
552
|
+
try {
|
|
553
|
+
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
554
|
+
headerRow,
|
|
555
|
+
maxSheetSize: this._maxSheetSize,
|
|
556
|
+
sheetChunkSize: this._sheetChunkSize
|
|
557
|
+
});
|
|
558
|
+
const sheetNames = Object.keys(sheets);
|
|
559
|
+
await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
|
|
560
|
+
if (sheetNames.length > 1) {
|
|
561
|
+
this._renderMultiSheet(sheets, this._rawFileData.file.name);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
|
|
565
|
+
}
|
|
566
|
+
this._reshapeModal.closeModal();
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
])
|
|
574
|
+
.render(document.body);
|
|
575
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
576
|
+
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
577
|
+
// Update preview on header row change
|
|
578
|
+
const updatePreview = async () => {
|
|
579
|
+
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
580
|
+
try {
|
|
581
|
+
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
582
|
+
headerRow,
|
|
583
|
+
maxSheetSize: 10
|
|
584
|
+
});
|
|
585
|
+
const firstSheet = Object.values(sheets)[0];
|
|
586
|
+
if (!firstSheet) {
|
|
587
|
+
previewDiv.textContent = '⚠️ No data found';
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
|
|
591
|
+
const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' │ ');
|
|
592
|
+
return `${i === 0 ? '📌 ' : ' '}${cols}`;
|
|
593
|
+
}).join('\n');
|
|
594
|
+
previewDiv.textContent = `Columns: ${firstSheet.columns.join(' │ ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
previewDiv.textContent = `⚠️ Error: ${err.message}`;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
headerRowInput.addEventListener('input', updatePreview);
|
|
601
|
+
updatePreview();
|
|
602
|
+
this._reshapeModal.open();
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
606
|
+
*/
|
|
607
|
+
_showCSVReshapeModal() {
|
|
608
|
+
if (!this._rawFileData)
|
|
609
|
+
return;
|
|
610
|
+
// Create modal if not exists
|
|
611
|
+
if (!this._reshapeModal) {
|
|
612
|
+
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
613
|
+
title: 'CSV Import Settings',
|
|
614
|
+
size: 'large',
|
|
615
|
+
close: true,
|
|
616
|
+
backdropClose: false
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// Build modal content
|
|
620
|
+
const modalContent = document.createElement('div');
|
|
621
|
+
modalContent.innerHTML = `
|
|
622
|
+
<div style="margin-bottom: 1rem;">
|
|
623
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
624
|
+
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
625
|
+
<option value=",">Comma (,)</option>
|
|
626
|
+
<option value="|">Pipe (|)</option>
|
|
627
|
+
<option value="\t">Tab (\\t)</option>
|
|
628
|
+
<option value=";">Semicolon (;)</option>
|
|
629
|
+
</select>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<div style="margin-bottom: 1rem;">
|
|
633
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
634
|
+
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div style="margin-bottom: 1rem;">
|
|
638
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
639
|
+
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<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;">
|
|
643
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
644
|
+
<div id="${this._id}-preview" style="font-family: 'JetBrains Mono', 'Courier New', monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--foreground)); line-height: 1.5;"></div>
|
|
528
645
|
</div>
|
|
529
646
|
`;
|
|
530
|
-
|
|
647
|
+
this._reshapeModal
|
|
648
|
+
.content(modalContent.innerHTML)
|
|
649
|
+
.actions([
|
|
650
|
+
{
|
|
651
|
+
label: 'Cancel',
|
|
652
|
+
variant: 'secondary',
|
|
653
|
+
click: () => this._reshapeModal.closeModal()
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
label: 'Apply & Re-import',
|
|
657
|
+
variant: 'primary',
|
|
658
|
+
click: async () => {
|
|
659
|
+
if (!this._rawFileData?.text)
|
|
660
|
+
return;
|
|
661
|
+
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
662
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
663
|
+
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
664
|
+
const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
|
|
665
|
+
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
666
|
+
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
667
|
+
this.state.loading = true;
|
|
668
|
+
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
669
|
+
try {
|
|
670
|
+
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
671
|
+
delimiter: delim,
|
|
672
|
+
headerRow,
|
|
673
|
+
skipRows,
|
|
674
|
+
hasHeader: true
|
|
675
|
+
});
|
|
676
|
+
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
677
|
+
this._setDataFrame(df, this._rawFileData.file.name);
|
|
678
|
+
this._reshapeModal.closeModal();
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
])
|
|
686
|
+
.render(document.body);
|
|
531
687
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
532
688
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
533
689
|
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
534
690
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
535
|
-
//
|
|
691
|
+
// Auto-detect initial values
|
|
536
692
|
if (this._rawFileData.text) {
|
|
537
693
|
const detected = this._driver._detectDelimiter(this._rawFileData.text);
|
|
538
694
|
delimiterSelect.value = detected === '\t' ? '\\t' : detected;
|
|
539
695
|
const headerRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
|
|
540
696
|
headerRowInput.value = String(headerRow);
|
|
541
697
|
}
|
|
542
|
-
//
|
|
698
|
+
// Update preview on changes
|
|
543
699
|
const updatePreview = async () => {
|
|
544
700
|
if (!this._rawFileData?.text)
|
|
545
701
|
return;
|
|
@@ -552,13 +708,13 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
552
708
|
headerRow,
|
|
553
709
|
skipRows,
|
|
554
710
|
hasHeader: true,
|
|
555
|
-
maxRows: 10
|
|
711
|
+
maxRows: 10
|
|
556
712
|
});
|
|
557
713
|
const preview = df.toRows().map((row, i) => {
|
|
558
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
714
|
+
const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' │ ');
|
|
559
715
|
return `${i === 0 ? '📌 ' : ' '}${cols}`;
|
|
560
716
|
}).join('\n');
|
|
561
|
-
previewDiv.textContent = `Columns: ${df.columns.join('
|
|
717
|
+
previewDiv.textContent = `Columns: ${df.columns.join(' │ ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
562
718
|
}
|
|
563
719
|
catch (err) {
|
|
564
720
|
previewDiv.textContent = `⚠️ Error: ${err.message}`;
|
|
@@ -568,36 +724,9 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
568
724
|
headerRowInput.addEventListener('input', updatePreview);
|
|
569
725
|
skipRowsInput.addEventListener('input', updatePreview);
|
|
570
726
|
updatePreview();
|
|
571
|
-
|
|
572
|
-
document.getElementById(`${this._id}-apply-reshape`).addEventListener('click', async () => {
|
|
573
|
-
if (!this._rawFileData?.text)
|
|
574
|
-
return;
|
|
575
|
-
const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
|
|
576
|
-
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
577
|
-
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
578
|
-
this.state.loading = true;
|
|
579
|
-
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
580
|
-
try {
|
|
581
|
-
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
582
|
-
delimiter: delim,
|
|
583
|
-
headerRow,
|
|
584
|
-
skipRows,
|
|
585
|
-
hasHeader: true
|
|
586
|
-
});
|
|
587
|
-
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
588
|
-
this._setDataFrame(df, this._rawFileData.file.name);
|
|
589
|
-
modal.remove();
|
|
590
|
-
}
|
|
591
|
-
catch (err) {
|
|
592
|
-
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
// ✅ Cancel/Close buttons
|
|
596
|
-
const closeModal = () => modal.remove();
|
|
597
|
-
document.getElementById(`${this._id}-cancel-reshape`).addEventListener('click', closeModal);
|
|
598
|
-
document.getElementById(`${this._id}-modal-close`).addEventListener('click', closeModal);
|
|
727
|
+
this._reshapeModal.open();
|
|
599
728
|
}
|
|
600
|
-
update(
|
|
729
|
+
update(_prop, _value) { }
|
|
601
730
|
/* ═══════════════════════════════════════════════════
|
|
602
731
|
* RENDER
|
|
603
732
|
* ═══════════════════════════════════════════════════ */
|
|
@@ -633,6 +762,8 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
633
762
|
const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
|
|
634
763
|
file.name.toLowerCase().endsWith('.xls');
|
|
635
764
|
if (isExcel) {
|
|
765
|
+
// ✅ Store raw file for reshape
|
|
766
|
+
this._rawFileData = { file, isExcel: true };
|
|
636
767
|
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
637
768
|
maxSheetSize: this._maxSheetSize,
|
|
638
769
|
sheetChunkSize: this._sheetChunkSize,
|