juxscript 1.1.185 → 1.1.187
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 +10 -6
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +317 -260
- package/lib/components/dataframe.ts +332 -277
- package/lib/styles/shadcn.css +17 -0
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ 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';
|
|
9
|
+
import { button } from './button.js'; // ✅ Import button factory
|
|
8
10
|
|
|
9
11
|
const TRIGGER_EVENTS = [] as const;
|
|
10
12
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'] as const;
|
|
@@ -39,7 +41,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
39
41
|
private _df: DataFrame | null = null;
|
|
40
42
|
private _driver: TabularDriver;
|
|
41
43
|
private _table: Table | null = null;
|
|
42
|
-
private _tabs: Tabs | null = null; // ✅ NEW: Tabs for multi-sheet Excel
|
|
44
|
+
private _tabs: Tabs | null = null; // ✅ NEW: Tabs for multi-sheet Excel // @ts-ignore used for multi-sheet
|
|
43
45
|
private _sheets: Map<string, DataFrame> = new Map(); // ✅ NEW: Store all sheets
|
|
44
46
|
private _tableOptions: {
|
|
45
47
|
striped: boolean;
|
|
@@ -49,7 +51,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
49
51
|
paginated: boolean;
|
|
50
52
|
rowsPerPage: number;
|
|
51
53
|
};
|
|
52
|
-
private _uploadRef: FileUpload | null = null;
|
|
54
|
+
private _uploadRef: FileUpload | null = null; // @ts-ignore used for reference tracking
|
|
53
55
|
private _storageKey: string | null = null;
|
|
54
56
|
private _pendingSource: (() => Promise<void>) | null = null;
|
|
55
57
|
private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
|
|
@@ -59,7 +61,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
59
61
|
private _sheetChunkSize: number = 10000; // ✅ Default 10k chunk
|
|
60
62
|
private _maxFileSize: number = 50; // ✅ Default 50MB
|
|
61
63
|
private _showReshapeWarning: boolean = true;
|
|
62
|
-
private _rawFileData: { file: File; text?: string; isExcel?: boolean } | null = null;
|
|
64
|
+
private _rawFileData: { file: File; text?: string; isExcel?: boolean } | null = null;
|
|
65
|
+
private _reshapeModal: Modal | null = null; // ✅ ADD THIS LINE
|
|
66
|
+
private _reshapeModalRendered: boolean = false; // Track if modal already rendered
|
|
63
67
|
|
|
64
68
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
65
69
|
super(id, {
|
|
@@ -454,7 +458,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
454
458
|
* INTERNAL
|
|
455
459
|
* ═══════════════════════════════════════════════════ */
|
|
456
460
|
|
|
457
|
-
private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' = 'empty'): void {
|
|
461
|
+
private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' | 'warning' = 'empty'): void {
|
|
458
462
|
const el = document.getElementById(`${this._id}-status`);
|
|
459
463
|
if (!el) return;
|
|
460
464
|
|
|
@@ -463,7 +467,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
463
467
|
|
|
464
468
|
el.innerHTML = '';
|
|
465
469
|
|
|
466
|
-
if (this._icon && type === 'success') {
|
|
470
|
+
if (this._icon && (type === 'success' || type === 'warning')) {
|
|
467
471
|
const iconEl = renderIcon(this._icon);
|
|
468
472
|
iconEl.style.width = '16px';
|
|
469
473
|
iconEl.style.height = '16px';
|
|
@@ -490,117 +494,116 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
490
494
|
this._df = df.select(...cleanCols);
|
|
491
495
|
}
|
|
492
496
|
|
|
493
|
-
|
|
494
|
-
this.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
// Update the table with new data
|
|
498
|
+
if (this._table && this._df) {
|
|
499
|
+
const columnDefs = this._df.columns.map(col => ({
|
|
500
|
+
key: col,
|
|
501
|
+
label: col
|
|
502
|
+
}));
|
|
503
|
+
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
504
|
+
}
|
|
498
505
|
|
|
499
|
-
// ✅
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
506
|
+
// ✅ Detect malformed data
|
|
507
|
+
const isMalformed = this._detectMalformedData(this._df!);
|
|
508
|
+
|
|
509
|
+
// ✅ Show warning if malformed
|
|
510
|
+
if (isMalformed && this._showReshapeWarning && this._rawFileData) {
|
|
511
|
+
this._updateStatus(
|
|
512
|
+
`⚠️ ${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols (Data may be malformed — headers may be on wrong row)`,
|
|
513
|
+
'warning'
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Add Fix Import Settings button after a tick to ensure status DOM is ready
|
|
517
|
+
requestAnimationFrame(() => {
|
|
518
|
+
const statusEl = document.getElementById(`${this._id}-status`);
|
|
519
|
+
if (statusEl) {
|
|
520
|
+
const settingsBtn = document.createElement('button');
|
|
521
|
+
settingsBtn.textContent = '⚙️ Fix Import Settings';
|
|
522
|
+
settingsBtn.className = 'jux-button jux-button-sm jux-button-warning';
|
|
523
|
+
settingsBtn.style.marginLeft = '0.5rem';
|
|
524
|
+
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
525
|
+
statusEl.appendChild(settingsBtn);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
this._updateStatus(
|
|
530
|
+
`${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
|
|
531
|
+
'success'
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// ✅ Still add Settings button for manual adjustment
|
|
535
|
+
if (this._showReshapeWarning && this._rawFileData) {
|
|
536
|
+
requestAnimationFrame(() => {
|
|
537
|
+
const statusEl = document.getElementById(`${this._id}-status`);
|
|
538
|
+
if (statusEl) {
|
|
539
|
+
const settingsBtn = document.createElement('button');
|
|
540
|
+
settingsBtn.textContent = '⚙️ Settings';
|
|
541
|
+
settingsBtn.className = 'jux-button jux-button-sm jux-button-ghost';
|
|
542
|
+
settingsBtn.style.marginLeft = '0.5rem';
|
|
543
|
+
settingsBtn.addEventListener('click', () => this._showReshapeModal());
|
|
544
|
+
statusEl.appendChild(settingsBtn);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
509
547
|
}
|
|
510
548
|
}
|
|
511
549
|
|
|
512
550
|
this._triggerCallback('load', this._df, null, this);
|
|
513
551
|
}
|
|
514
552
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const tableElement = this._table['_tableElement'];
|
|
529
|
-
if (tableElement) {
|
|
530
|
-
const wrapper = tableElement.closest('.jux-table-wrapper') as HTMLElement;
|
|
531
|
-
if (wrapper) {
|
|
532
|
-
// Clear table content
|
|
533
|
-
tableElement.innerHTML = '';
|
|
534
|
-
|
|
535
|
-
// Rebuild header
|
|
536
|
-
const thead = (this._table as any)._buildTableHeader();
|
|
537
|
-
tableElement.appendChild(thead);
|
|
538
|
-
|
|
539
|
-
// Rebuild body
|
|
540
|
-
const tbody = document.createElement('tbody');
|
|
541
|
-
(this._table as any)._renderTableBody(tbody);
|
|
542
|
-
tableElement.appendChild(tbody);
|
|
543
|
-
|
|
544
|
-
// Re-wire events
|
|
545
|
-
(this._table as any)._wireTriggerEvents(tbody);
|
|
546
|
-
|
|
547
|
-
// ✅ FIX: Re-build pagination controls
|
|
548
|
-
if (this._tableOptions.paginated) {
|
|
549
|
-
(this._table as any)._updatePagination(wrapper, tbody);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
553
|
+
/**
|
|
554
|
+
* ✅ NEW: Detect if data looks malformed
|
|
555
|
+
*/
|
|
556
|
+
private _detectMalformedData(df: DataFrame): boolean {
|
|
557
|
+
const columns = df.columns;
|
|
558
|
+
const rows = df.toRows();
|
|
559
|
+
|
|
560
|
+
// Check 1: Columns have generic names like "__EMPTY", "_1", "col_0"
|
|
561
|
+
const hasGenericColumns = columns.some(col =>
|
|
562
|
+
col.startsWith('__EMPTY') ||
|
|
563
|
+
col.startsWith('_') ||
|
|
564
|
+
col.match(/^col_\d+$/)
|
|
565
|
+
);
|
|
554
566
|
|
|
555
|
-
|
|
556
|
-
const wrapper = document.getElementById(this._id);
|
|
557
|
-
if (!wrapper) return;
|
|
558
|
-
if (wrapper.querySelector('.jux-dataframe-filter')) return;
|
|
567
|
+
if (hasGenericColumns) return true;
|
|
559
568
|
|
|
560
|
-
|
|
561
|
-
|
|
569
|
+
// Check 2: First row values look like metadata (e.g., "Exported On:", "12/4/2025")
|
|
570
|
+
if (rows.length > 0) {
|
|
571
|
+
const firstRow = rows[0];
|
|
572
|
+
const values = Object.values(firstRow);
|
|
573
|
+
const hasMetadata = values.some(v =>
|
|
574
|
+
String(v).includes('Exported') ||
|
|
575
|
+
String(v).includes('Generated') ||
|
|
576
|
+
String(v).includes('Report')
|
|
577
|
+
);
|
|
562
578
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
input.placeholder = 'Filter rows...';
|
|
566
|
-
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
579
|
+
if (hasMetadata) return true;
|
|
580
|
+
}
|
|
567
581
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
582
|
+
// Check 3: Row 2 or 3 has values that look like headers (mostly strings, no numbers)
|
|
583
|
+
if (rows.length >= 3) {
|
|
584
|
+
const secondRow = rows[1];
|
|
585
|
+
const thirdRow = rows[2];
|
|
571
586
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
587
|
+
const checkRow = (row: Record<string, any>) => {
|
|
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;
|
|
575
593
|
|
|
576
|
-
|
|
577
|
-
|
|
594
|
+
return nonNumeric >= values.length * 0.7; // 70% non-numeric
|
|
595
|
+
};
|
|
578
596
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
} else {
|
|
583
|
-
wrapper.appendChild(filterContainer);
|
|
597
|
+
if (checkRow(secondRow) || checkRow(thirdRow)) {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
584
600
|
}
|
|
585
601
|
|
|
586
|
-
|
|
587
|
-
if (!this._df) return;
|
|
588
|
-
const text = input.value.toLowerCase();
|
|
589
|
-
if (!text) {
|
|
590
|
-
this._table?.rows(this._df.toRows());
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
const filtered = this._df.filter((row) => {
|
|
594
|
-
return Object.values(row).some(v =>
|
|
595
|
-
v !== null && v !== undefined && String(v).toLowerCase().includes(text)
|
|
596
|
-
);
|
|
597
|
-
});
|
|
598
|
-
this._table?.rows(filtered.toRows());
|
|
599
|
-
});
|
|
602
|
+
return false;
|
|
600
603
|
}
|
|
601
604
|
|
|
602
605
|
/**
|
|
603
|
-
* ✅ UPDATED: Show settings modal
|
|
606
|
+
* ✅ UPDATED: Show settings modal using Modal component
|
|
604
607
|
*/
|
|
605
608
|
private _showReshapeModal(): void {
|
|
606
609
|
if (!this._rawFileData) return;
|
|
@@ -608,252 +611,304 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
608
611
|
const isExcel = this._rawFileData.isExcel;
|
|
609
612
|
|
|
610
613
|
if (isExcel) {
|
|
611
|
-
// ✅ Excel-specific modal: Let user pick header row from sheet
|
|
612
614
|
this._showExcelReshapeModal();
|
|
613
615
|
} else {
|
|
614
|
-
// ✅ CSV-specific modal: delimiter + header row
|
|
615
616
|
this._showCSVReshapeModal();
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
/**
|
|
620
|
-
* ✅
|
|
621
|
+
* ✅ UPDATED: Excel reshape modal using Modal component
|
|
621
622
|
*/
|
|
622
623
|
private async _showExcelReshapeModal(): Promise<void> {
|
|
623
624
|
if (!this._rawFileData?.file) return;
|
|
624
625
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
626
|
+
// Remove old modal from DOM if it was previously rendered
|
|
627
|
+
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
628
|
+
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
629
|
+
if (oldEl) oldEl.remove();
|
|
630
|
+
this._reshapeModal = null;
|
|
631
|
+
this._reshapeModalRendered = false;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Create fresh modal
|
|
635
|
+
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
636
|
+
title: 'Excel Import Settings',
|
|
637
|
+
size: 'large',
|
|
638
|
+
close: true,
|
|
639
|
+
backdropClose: false
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Build modal content
|
|
643
|
+
const modalContent = document.createElement('div');
|
|
644
|
+
modalContent.innerHTML = `
|
|
645
|
+
<div style="margin-bottom: 1rem;">
|
|
646
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
647
|
+
Header Row (0-based)
|
|
648
|
+
</label>
|
|
649
|
+
<input
|
|
650
|
+
type="number"
|
|
651
|
+
id="${this._id}-header-row"
|
|
652
|
+
class="jux-input-element"
|
|
653
|
+
value="0"
|
|
654
|
+
min="0"
|
|
655
|
+
max="50"
|
|
656
|
+
style="width: 100%;"
|
|
657
|
+
/>
|
|
658
|
+
<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);">
|
|
659
|
+
⚠️ <strong>Detected issue:</strong> Row 0 contains metadata ("Exported On:"), row 2 contains actual headers ("340B ID"). Try setting this to <strong>2</strong>.
|
|
646
660
|
</div>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
661
|
+
</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;">
|
|
664
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
665
|
+
Preview (first 10 rows)
|
|
650
666
|
</div>
|
|
667
|
+
<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>
|
|
651
668
|
</div>
|
|
652
669
|
`;
|
|
653
670
|
|
|
654
|
-
|
|
671
|
+
this._reshapeModal
|
|
672
|
+
.content(modalContent.innerHTML)
|
|
673
|
+
.actions([
|
|
674
|
+
{
|
|
675
|
+
label: 'Cancel',
|
|
676
|
+
variant: 'secondary',
|
|
677
|
+
click: () => this._reshapeModal!.closeModal()
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
label: 'Apply & Re-import',
|
|
681
|
+
variant: 'primary',
|
|
682
|
+
click: async () => {
|
|
683
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
684
|
+
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
685
|
+
|
|
686
|
+
this.state.loading = true;
|
|
687
|
+
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
691
|
+
headerRow,
|
|
692
|
+
maxSheetSize: this._maxSheetSize,
|
|
693
|
+
sheetChunkSize: this._sheetChunkSize
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const sheetNames = Object.keys(sheets);
|
|
697
|
+
await this._driver.store(this._rawFileData!.file.name, sheets[sheetNames[0]], { source: this._rawFileData!.file.name });
|
|
698
|
+
|
|
699
|
+
if (sheetNames.length > 1) {
|
|
700
|
+
this._renderMultiSheet(sheets, this._rawFileData!.file.name);
|
|
701
|
+
} else {
|
|
702
|
+
this._showReshapeWarning = false; // Prevent recursive warning after manual fix
|
|
703
|
+
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData!.file.name);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this._reshapeModal!.closeModal();
|
|
707
|
+
} catch (err: any) {
|
|
708
|
+
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
]);
|
|
713
|
+
|
|
714
|
+
// Render modal to document.body and open it
|
|
715
|
+
this._reshapeModal.render(document.body);
|
|
716
|
+
this._reshapeModalRendered = true;
|
|
717
|
+
|
|
718
|
+
// Wait a tick for DOM to update after render
|
|
719
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
655
720
|
|
|
656
721
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
657
722
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
658
723
|
|
|
659
|
-
//
|
|
724
|
+
// Update preview on header row change
|
|
660
725
|
const updatePreview = async () => {
|
|
661
|
-
const headerRow = parseInt(headerRowInput
|
|
726
|
+
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
662
727
|
|
|
663
728
|
try {
|
|
664
|
-
// Re-parse Excel with specified header row
|
|
665
729
|
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
666
|
-
headerRow,
|
|
667
|
-
maxSheetSize: 10
|
|
730
|
+
headerRow,
|
|
731
|
+
maxSheetSize: 10
|
|
668
732
|
});
|
|
669
733
|
|
|
670
734
|
const firstSheet = Object.values(sheets)[0];
|
|
671
735
|
if (!firstSheet) {
|
|
672
|
-
previewDiv.textContent = '⚠️ No data found';
|
|
736
|
+
if (previewDiv) previewDiv.textContent = '⚠️ No data found';
|
|
673
737
|
return;
|
|
674
738
|
}
|
|
675
739
|
|
|
676
740
|
const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
|
|
677
|
-
const cols = Object.values(row).map(v => String(v).padEnd(20)).join('
|
|
741
|
+
const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' │ ');
|
|
678
742
|
return `${i === 0 ? '📌 ' : ' '}${cols}`;
|
|
679
743
|
}).join('\n');
|
|
680
744
|
|
|
681
|
-
previewDiv.textContent = `Columns: ${firstSheet.columns.join('
|
|
745
|
+
if (previewDiv) previewDiv.textContent = `Columns: ${firstSheet.columns.join(' │ ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
682
746
|
} catch (err: any) {
|
|
683
|
-
previewDiv.textContent = `⚠️ Error: ${err.message}`;
|
|
747
|
+
if (previewDiv) previewDiv.textContent = `⚠️ Error: ${err.message}`;
|
|
684
748
|
}
|
|
685
749
|
};
|
|
686
750
|
|
|
687
|
-
headerRowInput.addEventListener('input', updatePreview);
|
|
751
|
+
if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
|
|
688
752
|
updatePreview();
|
|
689
753
|
|
|
690
|
-
|
|
691
|
-
document.getElementById(`${this._id}-apply-reshape`)!.addEventListener('click', async () => {
|
|
692
|
-
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
693
|
-
|
|
694
|
-
this.state.loading = true;
|
|
695
|
-
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
696
|
-
|
|
697
|
-
try {
|
|
698
|
-
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
699
|
-
headerRow,
|
|
700
|
-
maxSheetSize: this._maxSheetSize,
|
|
701
|
-
sheetChunkSize: this._sheetChunkSize
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
const sheetNames = Object.keys(sheets);
|
|
705
|
-
await this._driver.store(this._rawFileData!.file.name, sheets[sheetNames[0]], { source: this._rawFileData!.file.name });
|
|
706
|
-
|
|
707
|
-
if (sheetNames.length > 1) {
|
|
708
|
-
this._renderMultiSheet(sheets, this._rawFileData!.file.name);
|
|
709
|
-
} else {
|
|
710
|
-
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData!.file.name);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
modal.remove();
|
|
714
|
-
} catch (err: any) {
|
|
715
|
-
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
// ✅ Cancel/Close buttons
|
|
720
|
-
const closeModal = () => modal.remove();
|
|
721
|
-
document.getElementById(`${this._id}-cancel-reshape`)!.addEventListener('click', closeModal);
|
|
722
|
-
document.getElementById(`${this._id}-modal-close`)!.addEventListener('click', closeModal);
|
|
754
|
+
this._reshapeModal.open();
|
|
723
755
|
}
|
|
724
756
|
|
|
725
757
|
/**
|
|
726
|
-
* ✅ CSV
|
|
758
|
+
* ✅ UPDATED: CSV reshape modal using Modal component
|
|
727
759
|
*/
|
|
728
760
|
private _showCSVReshapeModal(): void {
|
|
729
761
|
if (!this._rawFileData) return;
|
|
730
762
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
<button class="jux-modal-close" id="${this._id}-modal-close">×</button>
|
|
738
|
-
</div>
|
|
739
|
-
<div class="jux-modal-content" style="padding: 1.5rem;">
|
|
740
|
-
<div style="margin-bottom: 1rem;">
|
|
741
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
742
|
-
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
743
|
-
<option value=",">Comma (,)</option>
|
|
744
|
-
<option value="|">Pipe (|)</option>
|
|
745
|
-
<option value="\t">Tab (\\t)</option>
|
|
746
|
-
<option value=";">Semicolon (;)</option>
|
|
747
|
-
</select>
|
|
748
|
-
</div>
|
|
749
|
-
|
|
750
|
-
<div style="margin-bottom: 1rem;">
|
|
751
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
752
|
-
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
753
|
-
</div>
|
|
754
|
-
|
|
755
|
-
<div style="margin-bottom: 1rem;">
|
|
756
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
757
|
-
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
758
|
-
</div>
|
|
759
|
-
|
|
760
|
-
<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;">
|
|
761
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
762
|
-
<div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
|
|
763
|
-
</div>
|
|
764
|
-
</div>
|
|
765
|
-
<div class="jux-modal-footer">
|
|
766
|
-
<button id="${this._id}-cancel-reshape" class="jux-button jux-button-ghost">Cancel</button>
|
|
767
|
-
<button id="${this._id}-apply-reshape" class="jux-button">Apply & Re-import</button>
|
|
768
|
-
</div>
|
|
769
|
-
</div>
|
|
770
|
-
`;
|
|
771
|
-
|
|
772
|
-
document.body.appendChild(modal);
|
|
773
|
-
|
|
774
|
-
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
775
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
776
|
-
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
777
|
-
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
778
|
-
|
|
779
|
-
// ✅ Auto-detect initial values
|
|
780
|
-
if (this._rawFileData.text) {
|
|
781
|
-
const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
|
|
782
|
-
delimiterSelect.value = detected === '\t' ? '\\t' : detected;
|
|
783
|
-
|
|
784
|
-
const headerRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
|
|
785
|
-
headerRowInput.value = String(headerRow);
|
|
763
|
+
// Remove old modal from DOM if it was previously rendered
|
|
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;
|
|
786
769
|
}
|
|
787
770
|
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
771
|
+
// Create fresh modal
|
|
772
|
+
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
773
|
+
title: 'CSV Import Settings',
|
|
774
|
+
size: 'large',
|
|
775
|
+
close: true,
|
|
776
|
+
backdropClose: false
|
|
777
|
+
});
|
|
791
778
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
779
|
+
// Build modal content
|
|
780
|
+
const modalContent = document.createElement('div');
|
|
781
|
+
modalContent.innerHTML = `
|
|
782
|
+
<div style="margin-bottom: 1rem;">
|
|
783
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
784
|
+
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
785
|
+
<option value=",">Comma (,)</option>
|
|
786
|
+
<option value="|">Pipe (|)</option>
|
|
787
|
+
<option value="\t">Tab (\\t)</option>
|
|
788
|
+
<option value=";">Semicolon (;)</option>
|
|
789
|
+
</select>
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
<div style="margin-bottom: 1rem;">
|
|
793
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
|
|
794
|
+
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<div style="margin-bottom: 1rem;">
|
|
798
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
|
|
799
|
+
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
800
|
+
</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;">
|
|
803
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
804
|
+
<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>
|
|
805
|
+
</div>
|
|
806
|
+
`;
|
|
795
807
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
808
|
+
this._reshapeModal
|
|
809
|
+
.content(modalContent.innerHTML)
|
|
810
|
+
.actions([
|
|
811
|
+
{
|
|
812
|
+
label: 'Cancel',
|
|
813
|
+
variant: 'secondary',
|
|
814
|
+
click: () => this._reshapeModal!.closeModal()
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
label: 'Apply & Re-import',
|
|
818
|
+
variant: 'primary',
|
|
819
|
+
click: async () => {
|
|
820
|
+
if (!this._rawFileData?.text) return;
|
|
821
|
+
|
|
822
|
+
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
823
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
824
|
+
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
825
|
+
|
|
826
|
+
const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
|
|
827
|
+
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
828
|
+
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
829
|
+
|
|
830
|
+
this.state.loading = true;
|
|
831
|
+
this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
835
|
+
delimiter: delim,
|
|
836
|
+
headerRow,
|
|
837
|
+
skipRows,
|
|
838
|
+
hasHeader: true
|
|
839
|
+
});
|
|
804
840
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}).join('\n');
|
|
841
|
+
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
842
|
+
this._showReshapeWarning = false; // Prevent recursive warning after manual fix
|
|
843
|
+
this._setDataFrame(df, this._rawFileData.file.name);
|
|
809
844
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
845
|
+
this._reshapeModal!.closeModal();
|
|
846
|
+
} catch (err: any) {
|
|
847
|
+
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
]);
|
|
852
|
+
|
|
853
|
+
// Render modal to document.body and open it
|
|
854
|
+
this._reshapeModal.render(document.body);
|
|
855
|
+
this._reshapeModalRendered = true;
|
|
856
|
+
|
|
857
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
858
|
+
requestAnimationFrame(() => {
|
|
859
|
+
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
860
|
+
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
861
|
+
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
862
|
+
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
863
|
+
|
|
864
|
+
// Auto-detect initial values
|
|
865
|
+
if (this._rawFileData?.text) {
|
|
866
|
+
const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
|
|
867
|
+
if (delimiterSelect) delimiterSelect.value = detected === '\t' ? '\\t' : detected;
|
|
868
|
+
|
|
869
|
+
const headerRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
|
|
870
|
+
if (headerRowInput) headerRowInput.value = String(headerRow);
|
|
813
871
|
}
|
|
814
|
-
};
|
|
815
872
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
873
|
+
// Update preview on changes
|
|
874
|
+
const updatePreview = async () => {
|
|
875
|
+
if (!this._rawFileData?.text) return;
|
|
819
876
|
|
|
820
|
-
|
|
877
|
+
const delim = delimiterSelect?.value === '\\t' ? '\t' : (delimiterSelect?.value || ',');
|
|
878
|
+
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
879
|
+
const skipRows = parseInt(skipRowsInput?.value) || 0;
|
|
821
880
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
881
|
+
try {
|
|
882
|
+
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
883
|
+
delimiter: delim,
|
|
884
|
+
headerRow,
|
|
885
|
+
skipRows,
|
|
886
|
+
hasHeader: true,
|
|
887
|
+
maxRows: 10
|
|
888
|
+
});
|
|
825
889
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
890
|
+
const preview = df.toRows().map((row, i) => {
|
|
891
|
+
const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' │ ');
|
|
892
|
+
return `${i === 0 ? '📌 ' : ' '}${cols}`;
|
|
893
|
+
}).join('\n');
|
|
829
894
|
|
|
830
|
-
|
|
831
|
-
|
|
895
|
+
if (previewDiv) previewDiv.textContent = `Columns: ${df.columns.join(' │ ')}\n${'─'.repeat(80)}\n${preview}`;
|
|
896
|
+
} catch (err: any) {
|
|
897
|
+
if (previewDiv) previewDiv.textContent = `⚠️ Error: ${err.message}`;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
832
900
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
headerRow,
|
|
837
|
-
skipRows,
|
|
838
|
-
hasHeader: true
|
|
839
|
-
});
|
|
901
|
+
if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
|
|
902
|
+
if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
|
|
903
|
+
if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
|
|
840
904
|
|
|
841
|
-
|
|
842
|
-
this._setDataFrame(df, this._rawFileData.file.name);
|
|
905
|
+
updatePreview();
|
|
843
906
|
|
|
844
|
-
|
|
845
|
-
} catch (err: any) {
|
|
846
|
-
this._updateStatus(`❌ ${err.message}`, 'error');
|
|
847
|
-
}
|
|
907
|
+
this._reshapeModal!.open();
|
|
848
908
|
});
|
|
849
|
-
|
|
850
|
-
// ✅ Cancel/Close buttons
|
|
851
|
-
const closeModal = () => modal.remove();
|
|
852
|
-
document.getElementById(`${this._id}-cancel-reshape`)!.addEventListener('click', closeModal);
|
|
853
|
-
document.getElementById(`${this._id}-modal-close`)!.addEventListener('click', closeModal);
|
|
854
909
|
}
|
|
855
910
|
|
|
856
|
-
update(
|
|
911
|
+
update(_prop: string, _value: any): void { }
|
|
857
912
|
|
|
858
913
|
/* ═══════════════════════════════════════════════════
|
|
859
914
|
* RENDER
|