juxscript 1.1.205 → 1.1.206
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 +212 -242
- package/lib/components/dataframe.ts +229 -255
- package/package.json +1 -1
|
@@ -87,11 +87,16 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
87
87
|
private _updateStatus;
|
|
88
88
|
private _setDataFrame;
|
|
89
89
|
private _detectMalformedData;
|
|
90
|
-
private _detectLikelyHeaderRow;
|
|
91
90
|
private _showReshapeModal;
|
|
92
91
|
private _cleanupReshapeModal;
|
|
93
|
-
private _showExcelReshapeModal;
|
|
94
92
|
private _escapeHtml;
|
|
93
|
+
/**
|
|
94
|
+
* Build a clickable preview table from raw row data.
|
|
95
|
+
* Each row stores its actual sheet row index via data-sheet-row attribute.
|
|
96
|
+
* Returns the table HTML string.
|
|
97
|
+
*/
|
|
98
|
+
private _buildClickablePreviewHTML;
|
|
99
|
+
private _showExcelReshapeModal;
|
|
95
100
|
private _showCSVReshapeModal;
|
|
96
101
|
update(_prop: string, _value: any): void;
|
|
97
102
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -416,29 +416,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
416
416
|
}
|
|
417
417
|
return false;
|
|
418
418
|
}
|
|
419
|
-
_detectLikelyHeaderRow(df) {
|
|
420
|
-
const rows = df.toRows();
|
|
421
|
-
const cols = df.columns;
|
|
422
|
-
const colsAreGeneric = cols.some(c => c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/));
|
|
423
|
-
if (!colsAreGeneric)
|
|
424
|
-
return 0;
|
|
425
|
-
for (let i = 0; i < Math.min(rows.length, 10); i++) {
|
|
426
|
-
const row = rows[i];
|
|
427
|
-
const values = Object.values(row);
|
|
428
|
-
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
429
|
-
if (nonEmpty.length < values.length * 0.5)
|
|
430
|
-
continue;
|
|
431
|
-
const nonNumericCount = nonEmpty.filter(v => {
|
|
432
|
-
const str = String(v).trim();
|
|
433
|
-
return isNaN(Number(str)) && str !== '';
|
|
434
|
-
}).length;
|
|
435
|
-
if (nonNumericCount >= nonEmpty.length * 0.7) {
|
|
436
|
-
// toRows index i = file row (i + 1) since row 0 was used as headers
|
|
437
|
-
return i + 1;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return 0;
|
|
441
|
-
}
|
|
442
419
|
/* ═══════════════════════════════════════════════════
|
|
443
420
|
* RESHAPE MODAL
|
|
444
421
|
* ═══════════════════════════════════════════════════ */
|
|
@@ -461,23 +438,121 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
461
438
|
this._reshapeModalRendered = false;
|
|
462
439
|
}
|
|
463
440
|
}
|
|
441
|
+
_escapeHtml(text) {
|
|
442
|
+
const div = document.createElement('div');
|
|
443
|
+
div.textContent = text;
|
|
444
|
+
return div.innerHTML;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Build a clickable preview table from raw row data.
|
|
448
|
+
* Each row stores its actual sheet row index via data-sheet-row attribute.
|
|
449
|
+
* Returns the table HTML string.
|
|
450
|
+
*/
|
|
451
|
+
_buildClickablePreviewHTML(rawRows, selectedSheetRow) {
|
|
452
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
453
|
+
for (const { sheetRow, values } of rawRows) {
|
|
454
|
+
const isHeader = (sheetRow === selectedSheetRow);
|
|
455
|
+
const isSkipped = (sheetRow < selectedSheetRow);
|
|
456
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
|
|
457
|
+
if (isHeader) {
|
|
458
|
+
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
459
|
+
}
|
|
460
|
+
else if (isSkipped) {
|
|
461
|
+
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
462
|
+
}
|
|
463
|
+
html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
|
|
464
|
+
// Row index cell
|
|
465
|
+
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;">`;
|
|
466
|
+
if (isHeader) {
|
|
467
|
+
html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
html += `${sheetRow}`;
|
|
471
|
+
}
|
|
472
|
+
html += '</td>';
|
|
473
|
+
// Show first 6 columns
|
|
474
|
+
const displayCols = values.slice(0, 6);
|
|
475
|
+
displayCols.forEach(val => {
|
|
476
|
+
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
477
|
+
const cellStyle = isHeader
|
|
478
|
+
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
479
|
+
: 'padding: 8px 12px;';
|
|
480
|
+
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
481
|
+
});
|
|
482
|
+
if (values.length > 6) {
|
|
483
|
+
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
484
|
+
}
|
|
485
|
+
// Status badge
|
|
486
|
+
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
|
|
487
|
+
if (isHeader) {
|
|
488
|
+
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
489
|
+
}
|
|
490
|
+
else if (isSkipped) {
|
|
491
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
495
|
+
}
|
|
496
|
+
html += '</td></tr>';
|
|
497
|
+
}
|
|
498
|
+
html += '</table>';
|
|
499
|
+
return html;
|
|
500
|
+
}
|
|
464
501
|
async _showExcelReshapeModal() {
|
|
465
502
|
if (!this._rawFileData?.file)
|
|
466
503
|
return;
|
|
467
504
|
this._cleanupReshapeModal();
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
505
|
+
// Read raw cells from the file
|
|
506
|
+
const XLSX = await import('xlsx');
|
|
507
|
+
const buffer = await this._rawFileData.file.arrayBuffer();
|
|
508
|
+
const workbook = XLSX.read(buffer, {
|
|
509
|
+
type: 'array',
|
|
510
|
+
sheetRows: 20,
|
|
511
|
+
dense: false
|
|
512
|
+
});
|
|
513
|
+
const sheetName = workbook.SheetNames[0];
|
|
514
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
515
|
+
const ref = worksheet['!ref'];
|
|
516
|
+
if (!ref)
|
|
517
|
+
return;
|
|
518
|
+
const range = XLSX.utils.decode_range(ref);
|
|
519
|
+
const endRow = Math.min(range.e.r, 14); // Show up to 15 rows
|
|
520
|
+
const startCol = range.s.c;
|
|
521
|
+
const endCol = range.e.c;
|
|
522
|
+
const readCellValue = (r, c) => {
|
|
523
|
+
const addr = XLSX.utils.encode_cell({ r, c });
|
|
524
|
+
const cell = worksheet[addr];
|
|
525
|
+
if (!cell)
|
|
526
|
+
return null;
|
|
527
|
+
if (cell.w !== undefined)
|
|
528
|
+
return cell.w;
|
|
529
|
+
if (cell.v !== undefined)
|
|
530
|
+
return cell.v;
|
|
531
|
+
return null;
|
|
532
|
+
};
|
|
533
|
+
// Build raw row data with actual sheet row indices
|
|
534
|
+
const rawRows = [];
|
|
535
|
+
for (let r = 0; r <= endRow; r++) {
|
|
536
|
+
const values = [];
|
|
537
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
538
|
+
values.push(readCellValue(r, c));
|
|
477
539
|
}
|
|
540
|
+
rawRows.push({ sheetRow: r, values });
|
|
478
541
|
}
|
|
479
|
-
|
|
480
|
-
|
|
542
|
+
// Auto-detect best header row
|
|
543
|
+
let selectedSheetRow = 0;
|
|
544
|
+
for (const { sheetRow, values } of rawRows) {
|
|
545
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
546
|
+
if (nonEmpty.length < values.length * 0.5)
|
|
547
|
+
continue;
|
|
548
|
+
const nonNumeric = nonEmpty.filter(v => {
|
|
549
|
+
const str = String(v).trim();
|
|
550
|
+
return isNaN(Number(str)) && str !== '';
|
|
551
|
+
}).length;
|
|
552
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
553
|
+
selectedSheetRow = sheetRow;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
481
556
|
}
|
|
482
557
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
483
558
|
title: 'Excel Import Settings',
|
|
@@ -487,14 +562,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
487
562
|
});
|
|
488
563
|
const modalContentHTML = `
|
|
489
564
|
<div style="margin-bottom: 1rem;">
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
</label>
|
|
493
|
-
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
|
|
494
|
-
<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>
|
|
565
|
+
<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>
|
|
566
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
|
|
495
567
|
</div>
|
|
496
|
-
<div
|
|
497
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
568
|
+
<div>
|
|
569
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
498
570
|
<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>
|
|
499
571
|
</div>
|
|
500
572
|
`;
|
|
@@ -545,129 +617,73 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
545
617
|
this._reshapeModal.render(document.body);
|
|
546
618
|
this._reshapeModalRendered = true;
|
|
547
619
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
548
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
549
620
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
550
621
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
551
|
-
const
|
|
622
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
623
|
+
const updateHint = (row) => {
|
|
552
624
|
if (!hintDiv)
|
|
553
625
|
return;
|
|
554
|
-
|
|
555
|
-
|
|
626
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
627
|
+
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
628
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
629
|
+
if (row > 0) {
|
|
630
|
+
hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
|
|
556
631
|
}
|
|
557
632
|
else {
|
|
558
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row)
|
|
633
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
559
634
|
}
|
|
560
635
|
};
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
636
|
+
const renderPreview = (selected) => {
|
|
637
|
+
if (!previewDiv)
|
|
638
|
+
return;
|
|
639
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
640
|
+
// Wire click handlers on each row
|
|
641
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
642
|
+
tr.addEventListener('click', () => {
|
|
643
|
+
const rowIdx = parseInt(tr.dataset.sheetRow);
|
|
644
|
+
hiddenInput.value = String(rowIdx);
|
|
645
|
+
updateHint(rowIdx);
|
|
646
|
+
renderPreview(rowIdx);
|
|
572
647
|
});
|
|
573
|
-
|
|
574
|
-
const worksheet = workbook.Sheets[sheetName];
|
|
575
|
-
const ref = worksheet['!ref'];
|
|
576
|
-
if (!ref) {
|
|
577
|
-
if (previewDiv)
|
|
578
|
-
previewDiv.textContent = 'No data found';
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const range = XLSX.utils.decode_range(ref);
|
|
582
|
-
const endRow = range.e.r;
|
|
583
|
-
const startCol = range.s.c;
|
|
584
|
-
const endCol = range.e.c;
|
|
585
|
-
const readCellValue = (r, c) => {
|
|
586
|
-
const addr = XLSX.utils.encode_cell({ r, c });
|
|
587
|
-
const cell = worksheet[addr];
|
|
588
|
-
if (!cell)
|
|
589
|
-
return null;
|
|
590
|
-
if (cell.w !== undefined)
|
|
591
|
-
return cell.w;
|
|
592
|
-
if (cell.v !== undefined)
|
|
593
|
-
return cell.v;
|
|
594
|
-
return null;
|
|
595
|
-
};
|
|
596
|
-
const readRow = (r) => {
|
|
597
|
-
const vals = [];
|
|
598
|
-
for (let c = startCol; c <= endCol; c++) {
|
|
599
|
-
vals.push(readCellValue(r, c));
|
|
600
|
-
}
|
|
601
|
-
return vals;
|
|
602
|
-
};
|
|
603
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
604
|
-
const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
|
|
605
|
-
for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
|
|
606
|
-
const isHeader = (fileRow === headerRow);
|
|
607
|
-
const isSkipped = (fileRow < headerRow);
|
|
608
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
609
|
-
if (isHeader) {
|
|
610
|
-
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
611
|
-
}
|
|
612
|
-
else if (isSkipped) {
|
|
613
|
-
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
614
|
-
}
|
|
615
|
-
html += `<tr style="${rowStyle}">`;
|
|
616
|
-
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;">`;
|
|
617
|
-
if (isHeader) {
|
|
618
|
-
html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
html += `${fileRow}`;
|
|
622
|
-
}
|
|
623
|
-
html += '</td>';
|
|
624
|
-
const values = readRow(fileRow);
|
|
625
|
-
const displayCols = values.slice(0, 6);
|
|
626
|
-
displayCols.forEach(val => {
|
|
627
|
-
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
628
|
-
const cellStyle = isHeader
|
|
629
|
-
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
630
|
-
: 'padding: 8px 12px;';
|
|
631
|
-
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
632
|
-
});
|
|
633
|
-
if (values.length > 6) {
|
|
634
|
-
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
635
|
-
}
|
|
636
|
-
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
|
|
637
|
-
if (isHeader) {
|
|
638
|
-
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
639
|
-
}
|
|
640
|
-
else if (isSkipped) {
|
|
641
|
-
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
645
|
-
}
|
|
646
|
-
html += '</td></tr>';
|
|
647
|
-
}
|
|
648
|
-
html += '</table>';
|
|
649
|
-
if (previewDiv)
|
|
650
|
-
previewDiv.innerHTML = html;
|
|
651
|
-
}
|
|
652
|
-
catch (err) {
|
|
653
|
-
if (previewDiv)
|
|
654
|
-
previewDiv.textContent = `Error: ${err.message}`;
|
|
655
|
-
}
|
|
648
|
+
});
|
|
656
649
|
};
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
updatePreview();
|
|
650
|
+
updateHint(selectedSheetRow);
|
|
651
|
+
renderPreview(selectedSheetRow);
|
|
660
652
|
this._reshapeModal.open();
|
|
661
653
|
}
|
|
662
|
-
_escapeHtml(text) {
|
|
663
|
-
const div = document.createElement('div');
|
|
664
|
-
div.textContent = text;
|
|
665
|
-
return div.innerHTML;
|
|
666
|
-
}
|
|
667
654
|
_showCSVReshapeModal() {
|
|
668
|
-
if (!this._rawFileData)
|
|
655
|
+
if (!this._rawFileData?.text)
|
|
669
656
|
return;
|
|
670
657
|
this._cleanupReshapeModal();
|
|
658
|
+
const text = this._rawFileData.text;
|
|
659
|
+
const detected = this._driver._detectDelimiter(text);
|
|
660
|
+
// Parse raw lines
|
|
661
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
662
|
+
const rawRows = [];
|
|
663
|
+
const maxPreviewRows = Math.min(lines.length, 15);
|
|
664
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
665
|
+
if (!lines[i]) {
|
|
666
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const values = this._driver._parseLine(lines[i], detected);
|
|
670
|
+
rawRows.push({ sheetRow: i, values });
|
|
671
|
+
}
|
|
672
|
+
// Auto-detect header row
|
|
673
|
+
let selectedRow = 0;
|
|
674
|
+
for (const { sheetRow, values } of rawRows) {
|
|
675
|
+
const nonEmpty = values.filter((v) => v.trim() !== '');
|
|
676
|
+
if (nonEmpty.length < values.length * 0.5)
|
|
677
|
+
continue;
|
|
678
|
+
const nonNumeric = nonEmpty.filter((v) => {
|
|
679
|
+
const trimmed = v.trim();
|
|
680
|
+
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
681
|
+
}).length;
|
|
682
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
683
|
+
selectedRow = sheetRow;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
671
687
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
672
688
|
title: 'CSV Import Settings',
|
|
673
689
|
size: 'large',
|
|
@@ -685,12 +701,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
685
701
|
</select>
|
|
686
702
|
</div>
|
|
687
703
|
<div style="margin-bottom: 1rem;">
|
|
688
|
-
<
|
|
689
|
-
<input type="
|
|
704
|
+
<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>
|
|
705
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
|
|
690
706
|
</div>
|
|
691
|
-
<div
|
|
692
|
-
|
|
693
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
707
|
+
<div>
|
|
708
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
694
709
|
<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>
|
|
695
710
|
</div>
|
|
696
711
|
`;
|
|
@@ -706,12 +721,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
706
721
|
label: 'Apply & Re-import',
|
|
707
722
|
variant: 'primary',
|
|
708
723
|
click: async () => {
|
|
709
|
-
if (!this._rawFileData?.text)
|
|
710
|
-
return;
|
|
711
724
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
712
|
-
const
|
|
725
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
713
726
|
const delim = delimiterSelect.value;
|
|
714
|
-
const headerRow = parseInt(
|
|
727
|
+
const headerRow = parseInt(hiddenInput.value) || 0;
|
|
715
728
|
this.state.loading = true;
|
|
716
729
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
717
730
|
try {
|
|
@@ -735,102 +748,59 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
735
748
|
this._reshapeModalRendered = true;
|
|
736
749
|
requestAnimationFrame(() => {
|
|
737
750
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
738
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
739
751
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
740
752
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const detectedHeaderRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
|
|
746
|
-
if (headerRowInput)
|
|
747
|
-
headerRowInput.value = String(detectedHeaderRow);
|
|
748
|
-
}
|
|
749
|
-
const updateHint = () => {
|
|
753
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
754
|
+
if (delimiterSelect)
|
|
755
|
+
delimiterSelect.value = detected;
|
|
756
|
+
const updateHint = (row) => {
|
|
750
757
|
if (!hintDiv)
|
|
751
758
|
return;
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
759
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
760
|
+
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
761
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
762
|
+
if (row > 0) {
|
|
763
|
+
hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
|
|
755
764
|
}
|
|
756
765
|
else {
|
|
757
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row)
|
|
766
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
758
767
|
}
|
|
759
768
|
};
|
|
760
|
-
const
|
|
761
|
-
if (!this._rawFileData?.text)
|
|
762
|
-
return;
|
|
769
|
+
const reparse = () => {
|
|
763
770
|
const delim = delimiterSelect?.value || ',';
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
headerRow: 0,
|
|
770
|
-
hasHeader: true,
|
|
771
|
-
maxRows: headerRow + 10
|
|
772
|
-
});
|
|
773
|
-
const rawCols = rawDf.columns;
|
|
774
|
-
const rawRows = rawDf.toRows();
|
|
775
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
|
|
776
|
-
const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
|
|
777
|
-
for (let i = 0; i < totalRows; i++) {
|
|
778
|
-
const isHeader = (i === headerRow);
|
|
779
|
-
const isSkipped = (i < headerRow);
|
|
780
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
781
|
-
if (isHeader) {
|
|
782
|
-
rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
|
|
783
|
-
}
|
|
784
|
-
else if (isSkipped) {
|
|
785
|
-
rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
|
|
786
|
-
}
|
|
787
|
-
html += `<tr style="${rowStyle}">`;
|
|
788
|
-
html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
|
|
789
|
-
html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
|
|
790
|
-
html += '</td>';
|
|
791
|
-
let values;
|
|
792
|
-
if (i === 0) {
|
|
793
|
-
values = rawCols;
|
|
794
|
-
}
|
|
795
|
-
else if (i - 1 < rawRows.length) {
|
|
796
|
-
values = Object.values(rawRows[i - 1]);
|
|
797
|
-
}
|
|
798
|
-
else {
|
|
799
|
-
values = [];
|
|
800
|
-
}
|
|
801
|
-
values.slice(0, 6).forEach(val => {
|
|
802
|
-
const displayVal = val != null ? String(val).substring(0, 25) : '';
|
|
803
|
-
html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
|
|
804
|
-
});
|
|
805
|
-
if (values.length > 6) {
|
|
806
|
-
html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
807
|
-
}
|
|
808
|
-
html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
|
|
809
|
-
if (isHeader) {
|
|
810
|
-
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
811
|
-
}
|
|
812
|
-
else if (isSkipped) {
|
|
813
|
-
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
817
|
-
}
|
|
818
|
-
html += '</td></tr>';
|
|
771
|
+
rawRows.length = 0;
|
|
772
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
773
|
+
if (!lines[i]) {
|
|
774
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
775
|
+
continue;
|
|
819
776
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
previewDiv.innerHTML = html;
|
|
823
|
-
}
|
|
824
|
-
catch (err) {
|
|
825
|
-
if (previewDiv)
|
|
826
|
-
previewDiv.textContent = `Error: ${err.message}`;
|
|
777
|
+
const values = this._driver._parseLine(lines[i], delim);
|
|
778
|
+
rawRows.push({ sheetRow: i, values });
|
|
827
779
|
}
|
|
828
780
|
};
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
781
|
+
const renderPreview = (selected) => {
|
|
782
|
+
if (!previewDiv)
|
|
783
|
+
return;
|
|
784
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
785
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
786
|
+
tr.addEventListener('click', () => {
|
|
787
|
+
const rowIdx = parseInt(tr.dataset.sheetRow);
|
|
788
|
+
hiddenInput.value = String(rowIdx);
|
|
789
|
+
updateHint(rowIdx);
|
|
790
|
+
renderPreview(rowIdx);
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
};
|
|
794
|
+
if (delimiterSelect) {
|
|
795
|
+
delimiterSelect.addEventListener('change', () => {
|
|
796
|
+
reparse();
|
|
797
|
+
const current = parseInt(hiddenInput.value) || 0;
|
|
798
|
+
updateHint(current);
|
|
799
|
+
renderPreview(current);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
updateHint(selectedRow);
|
|
803
|
+
renderPreview(selectedRow);
|
|
834
804
|
this._reshapeModal.open();
|
|
835
805
|
});
|
|
836
806
|
}
|
|
@@ -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,132 @@ 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
|
+
// Read raw cells from the file
|
|
621
|
+
const XLSX = await import('xlsx');
|
|
622
|
+
const buffer = await this._rawFileData.file.arrayBuffer();
|
|
623
|
+
const workbook = XLSX.read(buffer, {
|
|
624
|
+
type: 'array',
|
|
625
|
+
sheetRows: 20,
|
|
626
|
+
dense: false
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const sheetName = workbook.SheetNames[0];
|
|
630
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
631
|
+
const ref = worksheet['!ref'];
|
|
632
|
+
if (!ref) return;
|
|
633
|
+
|
|
634
|
+
const range = XLSX.utils.decode_range(ref);
|
|
635
|
+
const endRow = Math.min(range.e.r, 14); // Show up to 15 rows
|
|
636
|
+
const startCol = range.s.c;
|
|
637
|
+
const endCol = range.e.c;
|
|
638
|
+
|
|
639
|
+
const readCellValue = (r: number, c: number): any => {
|
|
640
|
+
const addr = XLSX.utils.encode_cell({ r, c });
|
|
641
|
+
const cell = worksheet[addr];
|
|
642
|
+
if (!cell) return null;
|
|
643
|
+
if (cell.w !== undefined) return cell.w;
|
|
644
|
+
if (cell.v !== undefined) return cell.v;
|
|
645
|
+
return null;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Build raw row data with actual sheet row indices
|
|
649
|
+
const rawRows: { sheetRow: number; values: any[] }[] = [];
|
|
650
|
+
for (let r = 0; r <= endRow; r++) {
|
|
651
|
+
const values: any[] = [];
|
|
652
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
653
|
+
values.push(readCellValue(r, c));
|
|
654
|
+
}
|
|
655
|
+
rawRows.push({ sheetRow: r, values });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Auto-detect best header row
|
|
659
|
+
let selectedSheetRow = 0;
|
|
660
|
+
for (const { sheetRow, values } of rawRows) {
|
|
661
|
+
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
662
|
+
if (nonEmpty.length < values.length * 0.5) continue;
|
|
663
|
+
const nonNumeric = nonEmpty.filter(v => {
|
|
664
|
+
const str = String(v).trim();
|
|
665
|
+
return isNaN(Number(str)) && str !== '';
|
|
666
|
+
}).length;
|
|
667
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
668
|
+
selectedSheetRow = sheetRow;
|
|
669
|
+
break;
|
|
590
670
|
}
|
|
591
|
-
} catch {
|
|
592
|
-
suggestedRow = 0;
|
|
593
671
|
}
|
|
594
672
|
|
|
595
673
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
@@ -601,14 +679,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
601
679
|
|
|
602
680
|
const modalContentHTML = `
|
|
603
681
|
<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>
|
|
682
|
+
<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>
|
|
683
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
|
|
609
684
|
</div>
|
|
610
|
-
<div
|
|
611
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
685
|
+
<div>
|
|
686
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
612
687
|
<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
688
|
</div>
|
|
614
689
|
`;
|
|
@@ -668,134 +743,79 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
668
743
|
|
|
669
744
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
670
745
|
|
|
671
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
672
746
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
673
747
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
|
|
748
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
674
749
|
|
|
675
|
-
const updateHint = (
|
|
750
|
+
const updateHint = (row: number) => {
|
|
676
751
|
if (!hintDiv) return;
|
|
677
|
-
|
|
678
|
-
|
|
752
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
753
|
+
const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
|
|
754
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
755
|
+
if (row > 0) {
|
|
756
|
+
hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
|
|
679
757
|
} else {
|
|
680
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row)
|
|
758
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
681
759
|
}
|
|
682
760
|
};
|
|
683
761
|
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
dense: false
|
|
762
|
+
const renderPreview = (selected: number) => {
|
|
763
|
+
if (!previewDiv) return;
|
|
764
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
765
|
+
|
|
766
|
+
// Wire click handlers on each row
|
|
767
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
768
|
+
tr.addEventListener('click', () => {
|
|
769
|
+
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
770
|
+
hiddenInput.value = String(rowIdx);
|
|
771
|
+
updateHint(rowIdx);
|
|
772
|
+
renderPreview(rowIdx);
|
|
696
773
|
});
|
|
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
|
-
}
|
|
774
|
+
});
|
|
781
775
|
};
|
|
782
776
|
|
|
783
|
-
|
|
784
|
-
|
|
777
|
+
updateHint(selectedSheetRow);
|
|
778
|
+
renderPreview(selectedSheetRow);
|
|
785
779
|
this._reshapeModal.open();
|
|
786
780
|
}
|
|
787
781
|
|
|
788
|
-
private _escapeHtml(text: string): string {
|
|
789
|
-
const div = document.createElement('div');
|
|
790
|
-
div.textContent = text;
|
|
791
|
-
return div.innerHTML;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
782
|
private _showCSVReshapeModal(): void {
|
|
795
|
-
if (!this._rawFileData) return;
|
|
783
|
+
if (!this._rawFileData?.text) return;
|
|
796
784
|
|
|
797
785
|
this._cleanupReshapeModal();
|
|
798
786
|
|
|
787
|
+
const text = this._rawFileData.text;
|
|
788
|
+
const detected = (this._driver as any)._detectDelimiter(text);
|
|
789
|
+
|
|
790
|
+
// Parse raw lines
|
|
791
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
792
|
+
const rawRows: { sheetRow: number; values: any[] }[] = [];
|
|
793
|
+
const maxPreviewRows = Math.min(lines.length, 15);
|
|
794
|
+
|
|
795
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
796
|
+
if (!lines[i]) {
|
|
797
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const values = (this._driver as any)._parseLine(lines[i], detected);
|
|
801
|
+
rawRows.push({ sheetRow: i, values });
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Auto-detect header row
|
|
805
|
+
let selectedRow = 0;
|
|
806
|
+
for (const { sheetRow, values } of rawRows) {
|
|
807
|
+
const nonEmpty = values.filter((v: string) => v.trim() !== '');
|
|
808
|
+
if (nonEmpty.length < values.length * 0.5) continue;
|
|
809
|
+
const nonNumeric = nonEmpty.filter((v: string) => {
|
|
810
|
+
const trimmed = v.trim();
|
|
811
|
+
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
812
|
+
}).length;
|
|
813
|
+
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
814
|
+
selectedRow = sheetRow;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
799
819
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
800
820
|
title: 'CSV Import Settings',
|
|
801
821
|
size: 'large',
|
|
@@ -814,12 +834,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
814
834
|
</select>
|
|
815
835
|
</div>
|
|
816
836
|
<div style="margin-bottom: 1rem;">
|
|
817
|
-
<
|
|
818
|
-
<input type="
|
|
837
|
+
<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>
|
|
838
|
+
<input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
|
|
819
839
|
</div>
|
|
820
|
-
<div
|
|
821
|
-
|
|
822
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
840
|
+
<div>
|
|
841
|
+
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
823
842
|
<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
843
|
</div>
|
|
825
844
|
`;
|
|
@@ -836,26 +855,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
836
855
|
label: 'Apply & Re-import',
|
|
837
856
|
variant: 'primary',
|
|
838
857
|
click: async () => {
|
|
839
|
-
if (!this._rawFileData?.text) return;
|
|
840
|
-
|
|
841
858
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
842
|
-
const
|
|
859
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
843
860
|
|
|
844
861
|
const delim = delimiterSelect.value;
|
|
845
|
-
const headerRow = parseInt(
|
|
862
|
+
const headerRow = parseInt(hiddenInput.value) || 0;
|
|
846
863
|
|
|
847
864
|
this.state.loading = true;
|
|
848
865
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
849
866
|
|
|
850
867
|
try {
|
|
851
|
-
const df = this._driver.parseCSV(this._rawFileData
|
|
868
|
+
const df = this._driver.parseCSV(this._rawFileData!.text!, {
|
|
852
869
|
delimiter: delim,
|
|
853
870
|
headerRow,
|
|
854
871
|
hasHeader: true
|
|
855
872
|
});
|
|
856
873
|
|
|
857
|
-
await this._driver.store(this._rawFileData
|
|
858
|
-
this._setDataFrame(df, this._rawFileData
|
|
874
|
+
await this._driver.store(this._rawFileData!.file.name, df, { source: this._rawFileData!.file.name });
|
|
875
|
+
this._setDataFrame(df, this._rawFileData!.file.name);
|
|
859
876
|
this._reshapeModal!.closeModal();
|
|
860
877
|
} catch (err: any) {
|
|
861
878
|
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
@@ -870,105 +887,62 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
870
887
|
|
|
871
888
|
requestAnimationFrame(() => {
|
|
872
889
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
873
|
-
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
874
890
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
875
891
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
|
|
892
|
+
const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
876
893
|
|
|
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
|
-
}
|
|
894
|
+
if (delimiterSelect) delimiterSelect.value = detected;
|
|
884
895
|
|
|
885
|
-
const updateHint = () => {
|
|
896
|
+
const updateHint = (row: number) => {
|
|
886
897
|
if (!hintDiv) return;
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
898
|
+
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
899
|
+
const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
|
|
900
|
+
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
901
|
+
if (row > 0) {
|
|
902
|
+
hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
|
|
890
903
|
} else {
|
|
891
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row)
|
|
904
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
892
905
|
}
|
|
893
906
|
};
|
|
894
907
|
|
|
895
|
-
const
|
|
896
|
-
if (!this._rawFileData?.text) return;
|
|
897
|
-
|
|
908
|
+
const reparse = () => {
|
|
898
909
|
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>';
|
|
910
|
+
rawRows.length = 0;
|
|
911
|
+
for (let i = 0; i < maxPreviewRows; i++) {
|
|
912
|
+
if (!lines[i]) {
|
|
913
|
+
rawRows.push({ sheetRow: i, values: [''] });
|
|
914
|
+
continue;
|
|
959
915
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (previewDiv) previewDiv.innerHTML = html;
|
|
963
|
-
} catch (err: any) {
|
|
964
|
-
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
916
|
+
const values = (this._driver as any)._parseLine(lines[i], delim);
|
|
917
|
+
rawRows.push({ sheetRow: i, values });
|
|
965
918
|
}
|
|
966
919
|
};
|
|
967
920
|
|
|
968
|
-
|
|
969
|
-
|
|
921
|
+
const renderPreview = (selected: number) => {
|
|
922
|
+
if (!previewDiv) return;
|
|
923
|
+
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
924
|
+
|
|
925
|
+
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
926
|
+
tr.addEventListener('click', () => {
|
|
927
|
+
const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
|
|
928
|
+
hiddenInput.value = String(rowIdx);
|
|
929
|
+
updateHint(rowIdx);
|
|
930
|
+
renderPreview(rowIdx);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
if (delimiterSelect) {
|
|
936
|
+
delimiterSelect.addEventListener('change', () => {
|
|
937
|
+
reparse();
|
|
938
|
+
const current = parseInt(hiddenInput.value) || 0;
|
|
939
|
+
updateHint(current);
|
|
940
|
+
renderPreview(current);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
970
943
|
|
|
971
|
-
|
|
944
|
+
updateHint(selectedRow);
|
|
945
|
+
renderPreview(selectedRow);
|
|
972
946
|
this._reshapeModal!.open();
|
|
973
947
|
});
|
|
974
948
|
}
|