juxscript 1.1.205 → 1.1.207
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 +7 -2
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +185 -243
- package/lib/components/dataframe.ts +203 -255
- package/lib/storage/TabularDriver.d.ts +10 -4
- package/lib/storage/TabularDriver.d.ts.map +1 -1
- package/lib/storage/TabularDriver.js +61 -23
- package/lib/storage/TabularDriver.ts +57 -23
- package/package.json +1 -1
|
@@ -519,37 +519,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
519
519
|
return false;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
private _detectLikelyHeaderRow(df: DataFrame): number {
|
|
523
|
-
const rows = df.toRows();
|
|
524
|
-
const cols = df.columns;
|
|
525
|
-
|
|
526
|
-
const colsAreGeneric = cols.some(c =>
|
|
527
|
-
c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/)
|
|
528
|
-
);
|
|
529
|
-
|
|
530
|
-
if (!colsAreGeneric) return 0;
|
|
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() !== '');
|
|
536
|
-
|
|
537
|
-
if (nonEmpty.length < values.length * 0.5) continue;
|
|
538
|
-
|
|
539
|
-
const nonNumericCount = nonEmpty.filter(v => {
|
|
540
|
-
const str = String(v).trim();
|
|
541
|
-
return isNaN(Number(str)) && str !== '';
|
|
542
|
-
}).length;
|
|
543
|
-
|
|
544
|
-
if (nonNumericCount >= nonEmpty.length * 0.7) {
|
|
545
|
-
// toRows index i = file row (i + 1) since row 0 was used as headers
|
|
546
|
-
return i + 1;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return 0;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
522
|
/* ═══════════════════════════════════════════════════
|
|
554
523
|
* RESHAPE MODAL
|
|
555
524
|
* ═══════════════════════════════════════════════════ */
|
|
@@ -573,23 +542,103 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
573
542
|
}
|
|
574
543
|
}
|
|
575
544
|
|
|
545
|
+
private _escapeHtml(text: string): string {
|
|
546
|
+
const div = document.createElement('div');
|
|
547
|
+
div.textContent = text;
|
|
548
|
+
return div.innerHTML;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Build a clickable preview table from raw row data.
|
|
553
|
+
* Each row stores its actual sheet row index via data-sheet-row attribute.
|
|
554
|
+
* Returns the table HTML string.
|
|
555
|
+
*/
|
|
556
|
+
private _buildClickablePreviewHTML(
|
|
557
|
+
rawRows: { sheetRow: number; values: any[] }[],
|
|
558
|
+
selectedSheetRow: number
|
|
559
|
+
): string {
|
|
560
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
561
|
+
|
|
562
|
+
for (const { sheetRow, values } of rawRows) {
|
|
563
|
+
const isHeader = (sheetRow === selectedSheetRow);
|
|
564
|
+
const isSkipped = (sheetRow < selectedSheetRow);
|
|
565
|
+
|
|
566
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
|
|
567
|
+
|
|
568
|
+
if (isHeader) {
|
|
569
|
+
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
570
|
+
} else if (isSkipped) {
|
|
571
|
+
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
|
|
575
|
+
|
|
576
|
+
// Row index cell
|
|
577
|
+
html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center; user-select: none;">`;
|
|
578
|
+
if (isHeader) {
|
|
579
|
+
html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
|
|
580
|
+
} else {
|
|
581
|
+
html += `${sheetRow}`;
|
|
582
|
+
}
|
|
583
|
+
html += '</td>';
|
|
584
|
+
|
|
585
|
+
// Show first 6 columns
|
|
586
|
+
const displayCols = values.slice(0, 6);
|
|
587
|
+
displayCols.forEach(val => {
|
|
588
|
+
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
589
|
+
const cellStyle = isHeader
|
|
590
|
+
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
591
|
+
: 'padding: 8px 12px;';
|
|
592
|
+
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
if (values.length > 6) {
|
|
596
|
+
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Status badge
|
|
600
|
+
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
|
|
601
|
+
if (isHeader) {
|
|
602
|
+
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
603
|
+
} else if (isSkipped) {
|
|
604
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
605
|
+
} else {
|
|
606
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
607
|
+
}
|
|
608
|
+
html += '</td></tr>';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
html += '</table>';
|
|
612
|
+
return html;
|
|
613
|
+
}
|
|
614
|
+
|
|
576
615
|
private async _showExcelReshapeModal(): Promise<void> {
|
|
577
616
|
if (!this._rawFileData?.file) return;
|
|
578
617
|
|
|
579
618
|
this._cleanupReshapeModal();
|
|
580
619
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
620
|
+
// ✅ Use the SAME cell-reading method as the parser
|
|
621
|
+
const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
|
|
622
|
+
|
|
623
|
+
if (rawRows.length === 0) return;
|
|
624
|
+
|
|
625
|
+
// Log what we got so we can verify alignment
|
|
626
|
+
console.log('[DataFrame Preview] Raw rows from readRawExcelRows:');
|
|
627
|
+
rawRows.forEach(r => console.log(` sheetRow ${r.sheetRow}:`, r.values.slice(0, 5)));
|
|
628
|
+
|
|
629
|
+
// Auto-detect best header row
|
|
630
|
+
let selectedSheetRow = rawRows[0].sheetRow;
|
|
631
|
+
for (const { sheetRow, values } of rawRows) {
|
|
632
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
633
|
+
if (nonEmpty.length < values.length * 0.5) continue;
|
|
634
|
+
const nonNumeric = nonEmpty.filter(v => {
|
|
635
|
+
const str = String(v).trim();
|
|
636
|
+
return isNaN(Number(str)) && str !== '';
|
|
637
|
+
}).length;
|
|
638
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
639
|
+
selectedSheetRow = sheetRow;
|
|
640
|
+
break;
|
|
590
641
|
}
|
|
591
|
-
} catch {
|
|
592
|
-
suggestedRow = 0;
|
|
593
642
|
}
|
|
594
643
|
|
|
595
644
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
@@ -601,14 +650,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
601
650
|
|
|
602
651
|
const modalContentHTML = `
|
|
603
652
|
<div style="margin-bottom: 1rem;">
|
|
604
|
-
<
|
|
605
|
-
|
|
606
|
-
</label>
|
|
607
|
-
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
|
|
608
|
-
<div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
|
|
653
|
+
<div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
|
|
654
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
|
|
609
655
|
</div>
|
|
610
|
-
<div
|
|
611
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
656
|
+
<div>
|
|
657
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
612
658
|
<div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
|
|
613
659
|
</div>
|
|
614
660
|
`;
|
|
@@ -628,6 +674,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
628
674
|
const input = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
629
675
|
const headerRow = parseInt(input.value) || 0;
|
|
630
676
|
|
|
677
|
+
console.log(`[DataFrame] Apply clicked: headerRow=${headerRow}`);
|
|
678
|
+
|
|
631
679
|
this.state.loading = true;
|
|
632
680
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
633
681
|
|
|
@@ -668,134 +716,79 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
668
716
|
|
|
669
717
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
670
718
|
|
|
671
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
672
719
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
673
720
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
|
|
721
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
674
722
|
|
|
675
|
-
const updateHint = (
|
|
723
|
+
const updateHint = (row: number) => {
|
|
676
724
|
if (!hintDiv) return;
|
|
677
|
-
|
|
678
|
-
|
|
725
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
726
|
+
const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
|
|
727
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
728
|
+
if (row > rawRows[0].sheetRow) {
|
|
729
|
+
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
679
730
|
} else {
|
|
680
|
-
hintDiv.innerHTML = `
|
|
731
|
+
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
681
732
|
}
|
|
682
733
|
};
|
|
683
734
|
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
dense: false
|
|
735
|
+
const renderPreview = (selected: number) => {
|
|
736
|
+
if (!previewDiv) return;
|
|
737
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
738
|
+
|
|
739
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
740
|
+
tr.addEventListener('click', () => {
|
|
741
|
+
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
742
|
+
hiddenInput.value = String(rowIdx);
|
|
743
|
+
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
744
|
+
updateHint(rowIdx);
|
|
745
|
+
renderPreview(rowIdx);
|
|
696
746
|
});
|
|
697
|
-
|
|
698
|
-
const sheetName = workbook.SheetNames[0];
|
|
699
|
-
const worksheet = workbook.Sheets[sheetName];
|
|
700
|
-
const ref = worksheet['!ref'];
|
|
701
|
-
if (!ref) {
|
|
702
|
-
if (previewDiv) previewDiv.textContent = 'No data found';
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const range = XLSX.utils.decode_range(ref);
|
|
707
|
-
const endRow = range.e.r;
|
|
708
|
-
const startCol = range.s.c;
|
|
709
|
-
const endCol = range.e.c;
|
|
710
|
-
|
|
711
|
-
const readCellValue = (r: number, c: number): any => {
|
|
712
|
-
const addr = XLSX.utils.encode_cell({ r, c });
|
|
713
|
-
const cell = worksheet[addr];
|
|
714
|
-
if (!cell) return null;
|
|
715
|
-
if (cell.w !== undefined) return cell.w;
|
|
716
|
-
if (cell.v !== undefined) return cell.v;
|
|
717
|
-
return null;
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
const readRow = (r: number): any[] => {
|
|
721
|
-
const vals: any[] = [];
|
|
722
|
-
for (let c = startCol; c <= endCol; c++) {
|
|
723
|
-
vals.push(readCellValue(r, c));
|
|
724
|
-
}
|
|
725
|
-
return vals;
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
729
|
-
const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
|
|
730
|
-
|
|
731
|
-
for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
|
|
732
|
-
const isHeader = (fileRow === headerRow);
|
|
733
|
-
const isSkipped = (fileRow < headerRow);
|
|
734
|
-
|
|
735
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
736
|
-
if (isHeader) {
|
|
737
|
-
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
738
|
-
} else if (isSkipped) {
|
|
739
|
-
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
html += `<tr style="${rowStyle}">`;
|
|
743
|
-
html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center;">`;
|
|
744
|
-
if (isHeader) {
|
|
745
|
-
html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
|
|
746
|
-
} else {
|
|
747
|
-
html += `${fileRow}`;
|
|
748
|
-
}
|
|
749
|
-
html += '</td>';
|
|
750
|
-
|
|
751
|
-
const values = readRow(fileRow);
|
|
752
|
-
const displayCols = values.slice(0, 6);
|
|
753
|
-
displayCols.forEach(val => {
|
|
754
|
-
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
755
|
-
const cellStyle = isHeader
|
|
756
|
-
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
757
|
-
: 'padding: 8px 12px;';
|
|
758
|
-
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
if (values.length > 6) {
|
|
762
|
-
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
|
|
766
|
-
if (isHeader) {
|
|
767
|
-
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
768
|
-
} else if (isSkipped) {
|
|
769
|
-
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
770
|
-
} else {
|
|
771
|
-
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
772
|
-
}
|
|
773
|
-
html += '</td></tr>';
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
html += '</table>';
|
|
777
|
-
if (previewDiv) previewDiv.innerHTML = html;
|
|
778
|
-
} catch (err: any) {
|
|
779
|
-
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
780
|
-
}
|
|
747
|
+
});
|
|
781
748
|
};
|
|
782
749
|
|
|
783
|
-
|
|
784
|
-
|
|
750
|
+
updateHint(selectedSheetRow);
|
|
751
|
+
renderPreview(selectedSheetRow);
|
|
785
752
|
this._reshapeModal.open();
|
|
786
753
|
}
|
|
787
754
|
|
|
788
|
-
private _escapeHtml(text: string): string {
|
|
789
|
-
const div = document.createElement('div');
|
|
790
|
-
div.textContent = text;
|
|
791
|
-
return div.innerHTML;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
755
|
private _showCSVReshapeModal(): void {
|
|
795
|
-
if (!this._rawFileData) return;
|
|
756
|
+
if (!this._rawFileData?.text) return;
|
|
796
757
|
|
|
797
758
|
this._cleanupReshapeModal();
|
|
798
759
|
|
|
760
|
+
const text = this._rawFileData.text;
|
|
761
|
+
const detected = (this._driver as any)._detectDelimiter(text);
|
|
762
|
+
|
|
763
|
+
// Parse raw lines
|
|
764
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
765
|
+
const rawRows: { sheetRow: number; values: any[] }[] = [];
|
|
766
|
+
const maxPreviewRows = Math.min(lines.length, 15);
|
|
767
|
+
|
|
768
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
769
|
+
if (!lines[i]) {
|
|
770
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const values = (this._driver as any)._parseLine(lines[i], detected);
|
|
774
|
+
rawRows.push({ sheetRow: i, values });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Auto-detect header row
|
|
778
|
+
let selectedRow = 0;
|
|
779
|
+
for (const { sheetRow, values } of rawRows) {
|
|
780
|
+
const nonEmpty = values.filter((v: string) => v.trim() !== '');
|
|
781
|
+
if (nonEmpty.length < values.length * 0.5) continue;
|
|
782
|
+
const nonNumeric = nonEmpty.filter((v: string) => {
|
|
783
|
+
const trimmed = v.trim();
|
|
784
|
+
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
785
|
+
}).length;
|
|
786
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
787
|
+
selectedRow = sheetRow;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
799
792
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
800
793
|
title: 'CSV Import Settings',
|
|
801
794
|
size: 'large',
|
|
@@ -814,12 +807,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
814
807
|
</select>
|
|
815
808
|
</div>
|
|
816
809
|
<div style="margin-bottom: 1rem;">
|
|
817
|
-
<
|
|
818
|
-
<input type="
|
|
810
|
+
<div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
|
|
811
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
|
|
819
812
|
</div>
|
|
820
|
-
<div
|
|
821
|
-
|
|
822
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
813
|
+
<div>
|
|
814
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
823
815
|
<div id="${this._id}-preview" style="font-family: monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
|
|
824
816
|
</div>
|
|
825
817
|
`;
|
|
@@ -836,26 +828,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
836
828
|
label: 'Apply & Re-import',
|
|
837
829
|
variant: 'primary',
|
|
838
830
|
click: async () => {
|
|
839
|
-
if (!this._rawFileData?.text) return;
|
|
840
|
-
|
|
841
831
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
842
|
-
const
|
|
832
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
843
833
|
|
|
844
834
|
const delim = delimiterSelect.value;
|
|
845
|
-
const headerRow = parseInt(
|
|
835
|
+
const headerRow = parseInt(hiddenInput.value) || 0;
|
|
846
836
|
|
|
847
837
|
this.state.loading = true;
|
|
848
838
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
849
839
|
|
|
850
840
|
try {
|
|
851
|
-
const df = this._driver.parseCSV(this._rawFileData
|
|
841
|
+
const df = this._driver.parseCSV(this._rawFileData!.text!, {
|
|
852
842
|
delimiter: delim,
|
|
853
843
|
headerRow,
|
|
854
844
|
hasHeader: true
|
|
855
845
|
});
|
|
856
846
|
|
|
857
|
-
await this._driver.store(this._rawFileData
|
|
858
|
-
this._setDataFrame(df, this._rawFileData
|
|
847
|
+
await this._driver.store(this._rawFileData!.file.name, df, { source: this._rawFileData!.file.name });
|
|
848
|
+
this._setDataFrame(df, this._rawFileData!.file.name);
|
|
859
849
|
this._reshapeModal!.closeModal();
|
|
860
850
|
} catch (err: any) {
|
|
861
851
|
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
@@ -870,105 +860,63 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
870
860
|
|
|
871
861
|
requestAnimationFrame(() => {
|
|
872
862
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
873
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
874
863
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
875
864
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
|
|
865
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
876
866
|
|
|
877
|
-
if (
|
|
878
|
-
const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
|
|
879
|
-
if (delimiterSelect) delimiterSelect.value = detected;
|
|
880
|
-
|
|
881
|
-
const detectedHeaderRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
|
|
882
|
-
if (headerRowInput) headerRowInput.value = String(detectedHeaderRow);
|
|
883
|
-
}
|
|
867
|
+
if (delimiterSelect) delimiterSelect.value = detected;
|
|
884
868
|
|
|
885
|
-
const updateHint = () => {
|
|
869
|
+
const updateHint = (row: number) => {
|
|
886
870
|
if (!hintDiv) return;
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
871
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
872
|
+
const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
|
|
873
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
874
|
+
if (row > rawRows[0].sheetRow) {
|
|
875
|
+
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
890
876
|
} else {
|
|
891
|
-
hintDiv.innerHTML = `
|
|
877
|
+
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
892
878
|
}
|
|
893
879
|
};
|
|
894
880
|
|
|
895
|
-
const
|
|
896
|
-
if (!this._rawFileData?.text) return;
|
|
897
|
-
|
|
881
|
+
const reparse = () => {
|
|
898
882
|
const delim = delimiterSelect?.value || ',';
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
delimiter: delim,
|
|
905
|
-
headerRow: 0,
|
|
906
|
-
hasHeader: true,
|
|
907
|
-
maxRows: headerRow + 10
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
const rawCols = rawDf.columns;
|
|
911
|
-
const rawRows = rawDf.toRows();
|
|
912
|
-
|
|
913
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
|
|
914
|
-
const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
|
|
915
|
-
|
|
916
|
-
for (let i = 0; i < totalRows; i++) {
|
|
917
|
-
const isHeader = (i === headerRow);
|
|
918
|
-
const isSkipped = (i < headerRow);
|
|
919
|
-
|
|
920
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
921
|
-
if (isHeader) {
|
|
922
|
-
rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
|
|
923
|
-
} else if (isSkipped) {
|
|
924
|
-
rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
html += `<tr style="${rowStyle}">`;
|
|
928
|
-
html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
|
|
929
|
-
html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
|
|
930
|
-
html += '</td>';
|
|
931
|
-
|
|
932
|
-
let values: any[];
|
|
933
|
-
if (i === 0) {
|
|
934
|
-
values = rawCols;
|
|
935
|
-
} else if (i - 1 < rawRows.length) {
|
|
936
|
-
values = Object.values(rawRows[i - 1]);
|
|
937
|
-
} else {
|
|
938
|
-
values = [];
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
values.slice(0, 6).forEach(val => {
|
|
942
|
-
const displayVal = val != null ? String(val).substring(0, 25) : '';
|
|
943
|
-
html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
if (values.length > 6) {
|
|
947
|
-
html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
|
|
951
|
-
if (isHeader) {
|
|
952
|
-
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
953
|
-
} else if (isSkipped) {
|
|
954
|
-
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
955
|
-
} else {
|
|
956
|
-
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
957
|
-
}
|
|
958
|
-
html += '</td></tr>';
|
|
883
|
+
rawRows.length = 0;
|
|
884
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
885
|
+
if (!lines[i]) {
|
|
886
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
887
|
+
continue;
|
|
959
888
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (previewDiv) previewDiv.innerHTML = html;
|
|
963
|
-
} catch (err: any) {
|
|
964
|
-
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
889
|
+
const values = (this._driver as any)._parseLine(lines[i], delim);
|
|
890
|
+
rawRows.push({ sheetRow: i, values });
|
|
965
891
|
}
|
|
966
892
|
};
|
|
967
893
|
|
|
968
|
-
|
|
969
|
-
|
|
894
|
+
const renderPreview = (selected: number) => {
|
|
895
|
+
if (!previewDiv) return;
|
|
896
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
897
|
+
|
|
898
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
899
|
+
tr.addEventListener('click', () => {
|
|
900
|
+
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
901
|
+
hiddenInput.value = String(rowIdx);
|
|
902
|
+
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
903
|
+
updateHint(rowIdx);
|
|
904
|
+
renderPreview(rowIdx);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
if (delimiterSelect) {
|
|
910
|
+
delimiterSelect.addEventListener('change', () => {
|
|
911
|
+
reparse();
|
|
912
|
+
const current = parseInt(hiddenInput.value) || 0;
|
|
913
|
+
updateHint(current);
|
|
914
|
+
renderPreview(current);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
970
917
|
|
|
971
|
-
|
|
918
|
+
updateHint(selectedRow);
|
|
919
|
+
renderPreview(selectedRow);
|
|
972
920
|
this._reshapeModal!.open();
|
|
973
921
|
});
|
|
974
922
|
}
|
|
@@ -87,12 +87,18 @@ export declare class TabularDriver {
|
|
|
87
87
|
* Fetch and stream-parse a remote CSV/TSV file
|
|
88
88
|
*/
|
|
89
89
|
fetch(url: string, options?: ParseOptions): Promise<DataFrame>;
|
|
90
|
+
/**
|
|
91
|
+
* Read raw cell values from first sheet of an Excel file.
|
|
92
|
+
* Returns rows with their actual sheet row indices.
|
|
93
|
+
* Used by both the preview UI and the parser to ensure consistency.
|
|
94
|
+
*/
|
|
95
|
+
readRawExcelRows(file: File, maxRows?: number): Promise<{
|
|
96
|
+
sheetRow: number;
|
|
97
|
+
values: any[];
|
|
98
|
+
}[]>;
|
|
90
99
|
/**
|
|
91
100
|
* ✅ FIXED: Stream Excel file with optional headerRow override
|
|
92
|
-
* headerRow is
|
|
93
|
-
*
|
|
94
|
-
* Uses direct cell access instead of sheet_to_json to avoid
|
|
95
|
-
* issues with blank row handling and sparse arrays.
|
|
101
|
+
* headerRow is the absolute sheet row index (same as sheetRow from readRawExcelRows).
|
|
96
102
|
*/
|
|
97
103
|
streamFileMultiSheet(file: File, options?: ParseOptions): Promise<Record<string, DataFrame>>;
|
|
98
104
|
private _splitLines;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAA4B;gBAE3B,MAAM,GAAE,MAAsB,EAAE,SAAS,GAAE,MAAiB;IAKlE,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IA4BlC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgCxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,SAAS;IA6D7D;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IAoG5E;;OAEG;YACW,UAAU;IAuExB;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBlD;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBzF;;OAEG;IACG,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAwBjD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA4BzD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAqBlH;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IA0ExE
|
|
1
|
+
{"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"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"}
|