juxscript 1.1.196 → 1.1.198
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 +1 -0
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +214 -209
- package/lib/components/dataframe.ts +229 -229
- package/package.json +1 -1
|
@@ -91,6 +91,7 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
91
91
|
private _showReshapeModal;
|
|
92
92
|
private _cleanupReshapeModal;
|
|
93
93
|
private _showExcelReshapeModal;
|
|
94
|
+
private _escapeHtml;
|
|
94
95
|
private _showCSVReshapeModal;
|
|
95
96
|
update(_prop: string, _value: any): void;
|
|
96
97
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,9 @@ import { DataFrame } from '../storage/DataFrame.js';
|
|
|
3
3
|
import { TabularDriver } from '../storage/TabularDriver.js';
|
|
4
4
|
import { FileUpload } from './fileupload.js';
|
|
5
5
|
import { Table } from './table.js';
|
|
6
|
+
import { Tabs } from './tabs.js';
|
|
6
7
|
import { Modal } from './modal.js';
|
|
8
|
+
import { Button } from './button.js';
|
|
7
9
|
import { renderIcon } from './icons.js';
|
|
8
10
|
const TRIGGER_EVENTS = [];
|
|
9
11
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
@@ -249,65 +251,39 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
249
251
|
const wrapper = document.getElementById(this._id);
|
|
250
252
|
if (!wrapper)
|
|
251
253
|
return;
|
|
254
|
+
// Clean up existing content
|
|
252
255
|
const existingTable = wrapper.querySelector('.jux-table-wrapper');
|
|
253
256
|
if (existingTable)
|
|
254
257
|
existingTable.remove();
|
|
255
|
-
const existingTabs = wrapper.querySelector('.jux-
|
|
258
|
+
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
256
259
|
if (existingTabs)
|
|
257
260
|
existingTabs.remove();
|
|
258
261
|
Object.entries(sheets).forEach(([name, df]) => {
|
|
259
262
|
this._sheets.set(name, df);
|
|
260
263
|
});
|
|
261
264
|
const sheetNames = Object.keys(sheets);
|
|
262
|
-
// Build
|
|
265
|
+
// Build tabs using the Tabs component
|
|
266
|
+
const tabDefs = sheetNames.map(name => ({
|
|
267
|
+
id: name,
|
|
268
|
+
label: name,
|
|
269
|
+
content: '' // Content will be added after render
|
|
270
|
+
}));
|
|
271
|
+
this._tabs = new Tabs(`${this._id}-tabs`, {
|
|
272
|
+
tabs: tabDefs,
|
|
273
|
+
activeTab: sheetNames[0]
|
|
274
|
+
});
|
|
275
|
+
this._tabs.bind('tabChange', (tabId) => {
|
|
276
|
+
this._df = this._sheets.get(tabId) || null;
|
|
277
|
+
});
|
|
278
|
+
// Create container for tabs
|
|
263
279
|
const tabsContainer = document.createElement('div');
|
|
264
280
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
265
|
-
const tabList = document.createElement('div');
|
|
266
|
-
tabList.className = 'jux-tabs-list';
|
|
267
|
-
sheetNames.forEach((sheetName, idx) => {
|
|
268
|
-
const tabBtn = document.createElement('button');
|
|
269
|
-
tabBtn.className = 'jux-tabs-button' + (idx === 0 ? ' jux-tabs-button-active' : '');
|
|
270
|
-
tabBtn.setAttribute('data-sheet', sheetName);
|
|
271
|
-
const labelSpan = document.createElement('span');
|
|
272
|
-
labelSpan.textContent = sheetName;
|
|
273
|
-
tabBtn.appendChild(labelSpan);
|
|
274
|
-
// Add settings cog icon
|
|
275
|
-
const cogBtn = document.createElement('button');
|
|
276
|
-
cogBtn.className = 'jux-dataframe-tab-settings';
|
|
277
|
-
cogBtn.title = 'Import settings for ' + sheetName;
|
|
278
|
-
cogBtn.innerHTML = `<span class="iconify" data-icon="lucide:settings" style="width:14px;height:14px;"></span>`;
|
|
279
|
-
cogBtn.addEventListener('click', (e) => {
|
|
280
|
-
e.stopPropagation();
|
|
281
|
-
this._showReshapeModal();
|
|
282
|
-
});
|
|
283
|
-
tabBtn.appendChild(cogBtn);
|
|
284
|
-
tabBtn.addEventListener('click', () => {
|
|
285
|
-
// Switch active tab
|
|
286
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
287
|
-
btn.classList.remove('jux-tabs-button-active');
|
|
288
|
-
});
|
|
289
|
-
tabBtn.classList.add('jux-tabs-button-active');
|
|
290
|
-
// Show/hide panels
|
|
291
|
-
wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
292
|
-
panel.style.display = 'none';
|
|
293
|
-
});
|
|
294
|
-
const panel = document.getElementById(`${this._id}-panel-${sheetName}`);
|
|
295
|
-
if (panel)
|
|
296
|
-
panel.style.display = 'block';
|
|
297
|
-
// Update current df reference
|
|
298
|
-
this._df = this._sheets.get(sheetName) || null;
|
|
299
|
-
});
|
|
300
|
-
tabList.appendChild(tabBtn);
|
|
301
|
-
});
|
|
302
|
-
tabsContainer.appendChild(tabList);
|
|
303
281
|
wrapper.appendChild(tabsContainer);
|
|
304
|
-
|
|
282
|
+
this._tabs.render(tabsContainer);
|
|
283
|
+
// Now render tables into each tab panel
|
|
305
284
|
sheetNames.forEach((sheetName, idx) => {
|
|
306
285
|
const df = sheets[sheetName];
|
|
307
|
-
const
|
|
308
|
-
panel.className = 'jux-tabs-panel';
|
|
309
|
-
panel.id = `${this._id}-panel-${sheetName}`;
|
|
310
|
-
panel.style.display = idx === 0 ? 'block' : 'none';
|
|
286
|
+
const panelId = `${this._id}-tabs-${sheetName}-panel`;
|
|
311
287
|
const table = new Table(`${this._id}-table-${sheetName}`, {
|
|
312
288
|
striped: this._tableOptions.striped,
|
|
313
289
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -318,44 +294,49 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
318
294
|
});
|
|
319
295
|
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
320
296
|
table.columns(columnDefs).rows(df.toRows());
|
|
321
|
-
|
|
322
|
-
|
|
297
|
+
// Add settings button to tab panel
|
|
298
|
+
const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
|
|
299
|
+
label: '⚙️ Import Settings',
|
|
300
|
+
variant: 'ghost',
|
|
301
|
+
size: 'small'
|
|
302
|
+
});
|
|
303
|
+
settingsBtn.bind('click', () => this._showReshapeModal());
|
|
304
|
+
// Use addTabContent to add components
|
|
305
|
+
this._tabs.addTabContent(sheetName, [settingsBtn, table]);
|
|
323
306
|
if (this._tableOptions.filterable) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
307
|
+
// Add filter input above table
|
|
308
|
+
const panel = document.getElementById(panelId);
|
|
309
|
+
if (panel) {
|
|
310
|
+
const filterContainer = document.createElement('div');
|
|
311
|
+
filterContainer.className = 'jux-dataframe-filter';
|
|
312
|
+
const input = document.createElement('input');
|
|
313
|
+
input.type = 'text';
|
|
314
|
+
input.placeholder = `Filter ${sheetName}...`;
|
|
315
|
+
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
316
|
+
const iconEl = renderIcon('search');
|
|
317
|
+
iconEl.style.width = '16px';
|
|
318
|
+
iconEl.style.height = '16px';
|
|
319
|
+
const iconWrap = document.createElement('span');
|
|
320
|
+
iconWrap.className = 'jux-dataframe-filter-icon';
|
|
321
|
+
iconWrap.appendChild(iconEl);
|
|
322
|
+
filterContainer.appendChild(iconWrap);
|
|
323
|
+
filterContainer.appendChild(input);
|
|
324
|
+
input.addEventListener('input', () => {
|
|
325
|
+
const text = input.value.toLowerCase();
|
|
326
|
+
if (!text) {
|
|
327
|
+
table.rows(df.toRows());
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
|
|
331
|
+
table.rows(filtered.toRows());
|
|
332
|
+
});
|
|
333
|
+
const tableWrapper = panel.querySelector('.jux-table-wrapper');
|
|
334
|
+
if (tableWrapper) {
|
|
335
|
+
panel.insertBefore(filterContainer, tableWrapper);
|
|
343
336
|
}
|
|
344
|
-
const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
|
|
345
|
-
table.rows(filtered.toRows());
|
|
346
|
-
});
|
|
347
|
-
const tableWrapper = panel.querySelector('.jux-table-wrapper');
|
|
348
|
-
if (tableWrapper) {
|
|
349
|
-
panel.insertBefore(filterContainer, tableWrapper);
|
|
350
337
|
}
|
|
351
338
|
}
|
|
352
339
|
});
|
|
353
|
-
// Ensure Iconify renders the cog icons
|
|
354
|
-
requestAnimationFrame(() => {
|
|
355
|
-
if (window.Iconify) {
|
|
356
|
-
window.Iconify.scan();
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
340
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
360
341
|
this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
|
|
361
342
|
this._df = sheets[sheetNames[0]];
|
|
@@ -505,7 +486,21 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
505
486
|
if (!this._rawFileData?.file)
|
|
506
487
|
return;
|
|
507
488
|
this._cleanupReshapeModal();
|
|
508
|
-
|
|
489
|
+
// Always detect from a fresh raw parse, not from current _df
|
|
490
|
+
let suggestedRow = 0;
|
|
491
|
+
try {
|
|
492
|
+
const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
493
|
+
headerRow: 0,
|
|
494
|
+
maxSheetSize: 20
|
|
495
|
+
});
|
|
496
|
+
const rawSheet = Object.values(rawSheets)[0];
|
|
497
|
+
if (rawSheet) {
|
|
498
|
+
suggestedRow = this._detectLikelyHeaderRow(rawSheet);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
suggestedRow = 0;
|
|
503
|
+
}
|
|
509
504
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
510
505
|
title: 'Excel Import Settings',
|
|
511
506
|
size: 'large',
|
|
@@ -515,7 +510,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
515
510
|
const modalContentHTML = `
|
|
516
511
|
<div style="margin-bottom: 1rem;">
|
|
517
512
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
518
|
-
Header Row
|
|
513
|
+
Header Row (0-based index)
|
|
519
514
|
</label>
|
|
520
515
|
<input
|
|
521
516
|
type="number"
|
|
@@ -533,7 +528,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
533
528
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
534
529
|
Preview
|
|
535
530
|
</div>
|
|
536
|
-
<div id="${this._id}-preview" style="font-family: monospace; font-size:
|
|
531
|
+
<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>
|
|
537
532
|
</div>
|
|
538
533
|
`;
|
|
539
534
|
this._reshapeModal
|
|
@@ -585,20 +580,20 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
585
580
|
return;
|
|
586
581
|
if (headerRow > 0) {
|
|
587
582
|
hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
|
|
588
|
-
`Rows <strong>0–${headerRow - 1}</strong> will be skipped
|
|
583
|
+
`Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
|
|
589
584
|
}
|
|
590
585
|
else {
|
|
591
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers
|
|
586
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
|
|
592
587
|
}
|
|
593
588
|
};
|
|
594
589
|
const updatePreview = async () => {
|
|
595
590
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
596
591
|
updateHint(headerRow);
|
|
597
592
|
try {
|
|
598
|
-
//
|
|
593
|
+
// ALWAYS parse with headerRow=0 to get raw file structure
|
|
599
594
|
const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
600
595
|
headerRow: 0,
|
|
601
|
-
maxSheetSize: headerRow + 12
|
|
596
|
+
maxSheetSize: Math.max(headerRow + 12, 15)
|
|
602
597
|
});
|
|
603
598
|
const rawSheet = Object.values(rawSheets)[0];
|
|
604
599
|
if (!rawSheet) {
|
|
@@ -606,65 +601,72 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
606
601
|
previewDiv.textContent = 'No data found';
|
|
607
602
|
return;
|
|
608
603
|
}
|
|
604
|
+
// rawSheet.columns = row 0 values (when parsed with headerRow=0)
|
|
605
|
+
// rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
|
|
609
606
|
const rawCols = rawSheet.columns;
|
|
610
607
|
const rawRows = rawSheet.toRows();
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
// Rows 1 to headerRow-1 are skipped data rows
|
|
623
|
-
for (let i = 0; i < Math.min(headerRow - 1, rawRows.length); i++) {
|
|
624
|
-
const row = rawRows[i];
|
|
625
|
-
const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
|
|
626
|
-
lines.push(` [${i + 1}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
|
|
608
|
+
// Build HTML table showing raw file structure
|
|
609
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
610
|
+
// We need to show rows 0 through headerRow+7 (or so)
|
|
611
|
+
// Row 0 = rawCols, Row 1+ = rawRows[i-1]
|
|
612
|
+
const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
|
|
613
|
+
for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
|
|
614
|
+
const isHeader = (fileRow === headerRow);
|
|
615
|
+
const isSkipped = (fileRow < headerRow);
|
|
616
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
617
|
+
if (isHeader) {
|
|
618
|
+
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
627
619
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
lines.push(' (No data rows found after header)');
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
dataRows.forEach((row, i) => {
|
|
661
|
-
const rowIdx = headerRow + 1 + i;
|
|
662
|
-
const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
|
|
663
|
-
lines.push(` [${rowIdx}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
|
|
620
|
+
else if (isSkipped) {
|
|
621
|
+
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
622
|
+
}
|
|
623
|
+
html += `<tr style="${rowStyle}">`;
|
|
624
|
+
// Row index cell
|
|
625
|
+
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;">`;
|
|
626
|
+
if (isHeader) {
|
|
627
|
+
html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
html += `${fileRow}`;
|
|
631
|
+
}
|
|
632
|
+
html += '</td>';
|
|
633
|
+
// Get values for this file row
|
|
634
|
+
let values;
|
|
635
|
+
if (fileRow === 0) {
|
|
636
|
+
values = rawCols;
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
|
|
640
|
+
}
|
|
641
|
+
// Show first 6 columns
|
|
642
|
+
const displayCols = values.slice(0, 6);
|
|
643
|
+
displayCols.forEach(val => {
|
|
644
|
+
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
645
|
+
const cellStyle = isHeader
|
|
646
|
+
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
647
|
+
: 'padding: 8px 12px;';
|
|
648
|
+
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
664
649
|
});
|
|
650
|
+
if (values.length > 6) {
|
|
651
|
+
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
652
|
+
}
|
|
653
|
+
// Status badge cell
|
|
654
|
+
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
|
|
655
|
+
if (isHeader) {
|
|
656
|
+
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
657
|
+
}
|
|
658
|
+
else if (isSkipped) {
|
|
659
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
663
|
+
}
|
|
664
|
+
html += '</td>';
|
|
665
|
+
html += '</tr>';
|
|
665
666
|
}
|
|
667
|
+
html += '</table>';
|
|
666
668
|
if (previewDiv) {
|
|
667
|
-
previewDiv.
|
|
669
|
+
previewDiv.innerHTML = html;
|
|
668
670
|
}
|
|
669
671
|
}
|
|
670
672
|
catch (err) {
|
|
@@ -677,6 +679,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
677
679
|
updatePreview();
|
|
678
680
|
this._reshapeModal.open();
|
|
679
681
|
}
|
|
682
|
+
_escapeHtml(text) {
|
|
683
|
+
const div = document.createElement('div');
|
|
684
|
+
div.textContent = text;
|
|
685
|
+
return div.innerHTML;
|
|
686
|
+
}
|
|
680
687
|
_showCSVReshapeModal() {
|
|
681
688
|
if (!this._rawFileData)
|
|
682
689
|
return;
|
|
@@ -698,17 +705,13 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
698
705
|
</select>
|
|
699
706
|
</div>
|
|
700
707
|
<div style="margin-bottom: 1rem;">
|
|
701
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
|
|
708
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
|
|
702
709
|
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
703
710
|
</div>
|
|
704
|
-
<div style="margin-bottom: 1rem;">
|
|
705
|
-
|
|
706
|
-
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
707
|
-
</div>
|
|
708
|
-
<div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
|
|
709
|
-
<div class="jux-reshape-preview-container" style="margin-top: 1rem;">
|
|
711
|
+
<div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
|
|
712
|
+
<div class="jux-reshape-preview-container">
|
|
710
713
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
711
|
-
<div id="${this._id}-preview"
|
|
714
|
+
<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>
|
|
712
715
|
</div>
|
|
713
716
|
`;
|
|
714
717
|
this._reshapeModal
|
|
@@ -727,17 +730,14 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
727
730
|
return;
|
|
728
731
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
729
732
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
730
|
-
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
731
733
|
const delim = delimiterSelect.value;
|
|
732
734
|
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
733
|
-
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
734
735
|
this.state.loading = true;
|
|
735
736
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
736
737
|
try {
|
|
737
738
|
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
738
739
|
delimiter: delim,
|
|
739
740
|
headerRow,
|
|
740
|
-
skipRows,
|
|
741
741
|
hasHeader: true
|
|
742
742
|
});
|
|
743
743
|
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
@@ -755,7 +755,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
755
755
|
requestAnimationFrame(() => {
|
|
756
756
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
757
757
|
const headerRowInput = document.getElementById(`${this._id}-header-row`);
|
|
758
|
-
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
|
|
759
758
|
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
760
759
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
761
760
|
if (this._rawFileData?.text) {
|
|
@@ -770,75 +769,83 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
770
769
|
if (!hintDiv)
|
|
771
770
|
return;
|
|
772
771
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
772
|
+
if (headerRow > 0) {
|
|
773
|
+
hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
|
|
774
|
+
`Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
|
|
778
|
+
}
|
|
779
779
|
};
|
|
780
780
|
const updatePreview = () => {
|
|
781
781
|
if (!this._rawFileData?.text)
|
|
782
782
|
return;
|
|
783
783
|
const delim = delimiterSelect?.value || ',';
|
|
784
784
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
785
|
-
const skipRows = parseInt(skipRowsInput?.value) || 0;
|
|
786
785
|
updateHint();
|
|
787
786
|
try {
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
const lines = [];
|
|
791
|
-
const totalOffset = headerRow + skipRows;
|
|
792
|
-
// Parse raw (no header offset) to show skipped rows
|
|
793
|
-
if (totalOffset > 0) {
|
|
794
|
-
const rawDf = this._driver.parseCSV(this._rawFileData.text, {
|
|
795
|
-
delimiter: delim,
|
|
796
|
-
headerRow: 0,
|
|
797
|
-
skipRows: 0,
|
|
798
|
-
hasHeader: true,
|
|
799
|
-
maxRows: totalOffset + 1
|
|
800
|
-
});
|
|
801
|
-
lines.push('');
|
|
802
|
-
lines.push(` ┌${'─'.repeat(100)}┐`);
|
|
803
|
-
lines.push(` │ ROWS 0-${totalOffset - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
|
|
804
|
-
lines.push(` └${'─'.repeat(100)}┘`);
|
|
805
|
-
lines.push('');
|
|
806
|
-
const rawCols = rawDf.columns;
|
|
807
|
-
lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
|
|
808
|
-
const rawRows = rawDf.toRows();
|
|
809
|
-
const skippedCount = Math.min(totalOffset - 1, rawRows.length);
|
|
810
|
-
for (let i = 0; i < skippedCount; i++) {
|
|
811
|
-
const row = rawRows[i];
|
|
812
|
-
const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
|
|
813
|
-
const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
|
|
814
|
-
lines.push(`${rowIdx}${cols} ← skipped`);
|
|
815
|
-
}
|
|
816
|
-
lines.push('');
|
|
817
|
-
lines.push(` ╔${'═'.repeat(100)}╗`);
|
|
818
|
-
lines.push(` ║ ▼ DATA STARTS HERE (Row ${totalOffset} = Column Headers)`.padEnd(101) + '║');
|
|
819
|
-
lines.push(` ╚${'═'.repeat(100)}╝`);
|
|
820
|
-
lines.push('');
|
|
821
|
-
}
|
|
822
|
-
// Parse with actual settings for header + data rows
|
|
823
|
-
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
787
|
+
// Parse raw to show all rows
|
|
788
|
+
const rawDf = this._driver.parseCSV(this._rawFileData.text, {
|
|
824
789
|
delimiter: delim,
|
|
825
|
-
headerRow,
|
|
826
|
-
skipRows,
|
|
790
|
+
headerRow: 0,
|
|
827
791
|
hasHeader: true,
|
|
828
|
-
maxRows:
|
|
792
|
+
maxRows: headerRow + 10
|
|
829
793
|
});
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
794
|
+
const rawCols = rawDf.columns;
|
|
795
|
+
const rawRows = rawDf.toRows();
|
|
796
|
+
// Build HTML table
|
|
797
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
|
|
798
|
+
const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
|
|
799
|
+
for (let i = 0; i < totalRows; i++) {
|
|
800
|
+
const isHeader = (i === headerRow);
|
|
801
|
+
const isSkipped = (i < headerRow);
|
|
802
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
803
|
+
if (isHeader) {
|
|
804
|
+
rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
|
|
805
|
+
}
|
|
806
|
+
else if (isSkipped) {
|
|
807
|
+
rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
|
|
808
|
+
}
|
|
809
|
+
html += `<tr style="${rowStyle}">`;
|
|
810
|
+
// Row index
|
|
811
|
+
html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
|
|
812
|
+
html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
|
|
813
|
+
html += '</td>';
|
|
814
|
+
// Data
|
|
815
|
+
let values;
|
|
816
|
+
if (i === 0) {
|
|
817
|
+
values = rawCols;
|
|
818
|
+
}
|
|
819
|
+
else if (i - 1 < rawRows.length) {
|
|
820
|
+
values = Object.values(rawRows[i - 1]);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
values = [];
|
|
824
|
+
}
|
|
825
|
+
values.slice(0, 6).forEach(val => {
|
|
826
|
+
const displayVal = val != null ? String(val).substring(0, 25) : '';
|
|
827
|
+
html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
|
|
828
|
+
});
|
|
829
|
+
if (values.length > 6) {
|
|
830
|
+
html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
831
|
+
}
|
|
832
|
+
// Status
|
|
833
|
+
html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
|
|
834
|
+
if (isHeader) {
|
|
835
|
+
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
836
|
+
}
|
|
837
|
+
else if (isSkipped) {
|
|
838
|
+
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
842
|
+
}
|
|
843
|
+
html += '</td>';
|
|
844
|
+
html += '</tr>';
|
|
841
845
|
}
|
|
846
|
+
html += '</table>';
|
|
847
|
+
if (previewDiv)
|
|
848
|
+
previewDiv.innerHTML = html;
|
|
842
849
|
}
|
|
843
850
|
catch (err) {
|
|
844
851
|
if (previewDiv)
|
|
@@ -849,8 +856,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
849
856
|
delimiterSelect.addEventListener('change', updatePreview);
|
|
850
857
|
if (headerRowInput)
|
|
851
858
|
headerRowInput.addEventListener('input', updatePreview);
|
|
852
|
-
if (skipRowsInput)
|
|
853
|
-
skipRowsInput.addEventListener('input', updatePreview);
|
|
854
859
|
updatePreview();
|
|
855
860
|
this._reshapeModal.open();
|
|
856
861
|
});
|
|
@@ -5,6 +5,7 @@ import { FileUpload } from './fileupload.js';
|
|
|
5
5
|
import { Table } from './table.js';
|
|
6
6
|
import { Tabs } from './tabs.js';
|
|
7
7
|
import { Modal } from './modal.js';
|
|
8
|
+
import { Button } from './button.js';
|
|
8
9
|
import { renderIcon } from './icons.js';
|
|
9
10
|
|
|
10
11
|
const TRIGGER_EVENTS = [] as const;
|
|
@@ -311,10 +312,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
311
312
|
const wrapper = document.getElementById(this._id);
|
|
312
313
|
if (!wrapper) return;
|
|
313
314
|
|
|
315
|
+
// Clean up existing content
|
|
314
316
|
const existingTable = wrapper.querySelector('.jux-table-wrapper');
|
|
315
317
|
if (existingTable) existingTable.remove();
|
|
316
318
|
|
|
317
|
-
const existingTabs = wrapper.querySelector('.jux-
|
|
319
|
+
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
318
320
|
if (existingTabs) existingTabs.remove();
|
|
319
321
|
|
|
320
322
|
Object.entries(sheets).forEach(([name, df]) => {
|
|
@@ -323,65 +325,33 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
323
325
|
|
|
324
326
|
const sheetNames = Object.keys(sheets);
|
|
325
327
|
|
|
326
|
-
// Build
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
sheetNames.forEach((sheetName, idx) => {
|
|
334
|
-
const tabBtn = document.createElement('button');
|
|
335
|
-
tabBtn.className = 'jux-tabs-button' + (idx === 0 ? ' jux-tabs-button-active' : '');
|
|
336
|
-
tabBtn.setAttribute('data-sheet', sheetName);
|
|
337
|
-
|
|
338
|
-
const labelSpan = document.createElement('span');
|
|
339
|
-
labelSpan.textContent = sheetName;
|
|
340
|
-
tabBtn.appendChild(labelSpan);
|
|
341
|
-
|
|
342
|
-
// Add settings cog icon
|
|
343
|
-
const cogBtn = document.createElement('button');
|
|
344
|
-
cogBtn.className = 'jux-dataframe-tab-settings';
|
|
345
|
-
cogBtn.title = 'Import settings for ' + sheetName;
|
|
346
|
-
cogBtn.innerHTML = `<span class="iconify" data-icon="lucide:settings" style="width:14px;height:14px;"></span>`;
|
|
347
|
-
cogBtn.addEventListener('click', (e) => {
|
|
348
|
-
e.stopPropagation();
|
|
349
|
-
this._showReshapeModal();
|
|
350
|
-
});
|
|
351
|
-
tabBtn.appendChild(cogBtn);
|
|
352
|
-
|
|
353
|
-
tabBtn.addEventListener('click', () => {
|
|
354
|
-
// Switch active tab
|
|
355
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
356
|
-
btn.classList.remove('jux-tabs-button-active');
|
|
357
|
-
});
|
|
358
|
-
tabBtn.classList.add('jux-tabs-button-active');
|
|
359
|
-
|
|
360
|
-
// Show/hide panels
|
|
361
|
-
wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
362
|
-
(panel as HTMLElement).style.display = 'none';
|
|
363
|
-
});
|
|
364
|
-
const panel = document.getElementById(`${this._id}-panel-${sheetName}`);
|
|
365
|
-
if (panel) panel.style.display = 'block';
|
|
328
|
+
// Build tabs using the Tabs component
|
|
329
|
+
const tabDefs = sheetNames.map(name => ({
|
|
330
|
+
id: name,
|
|
331
|
+
label: name,
|
|
332
|
+
content: '' // Content will be added after render
|
|
333
|
+
}));
|
|
366
334
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
335
|
+
this._tabs = new Tabs(`${this._id}-tabs`, {
|
|
336
|
+
tabs: tabDefs,
|
|
337
|
+
activeTab: sheetNames[0]
|
|
338
|
+
});
|
|
370
339
|
|
|
371
|
-
|
|
340
|
+
this._tabs.bind('tabChange', (tabId: string) => {
|
|
341
|
+
this._df = this._sheets.get(tabId) || null;
|
|
372
342
|
});
|
|
373
343
|
|
|
374
|
-
|
|
344
|
+
// Create container for tabs
|
|
345
|
+
const tabsContainer = document.createElement('div');
|
|
346
|
+
tabsContainer.className = 'jux-dataframe-tabs';
|
|
375
347
|
wrapper.appendChild(tabsContainer);
|
|
376
348
|
|
|
377
|
-
|
|
349
|
+
this._tabs.render(tabsContainer);
|
|
350
|
+
|
|
351
|
+
// Now render tables into each tab panel
|
|
378
352
|
sheetNames.forEach((sheetName, idx) => {
|
|
379
353
|
const df = sheets[sheetName];
|
|
380
|
-
|
|
381
|
-
const panel = document.createElement('div');
|
|
382
|
-
panel.className = 'jux-tabs-panel';
|
|
383
|
-
panel.id = `${this._id}-panel-${sheetName}`;
|
|
384
|
-
panel.style.display = idx === 0 ? 'block' : 'none';
|
|
354
|
+
const panelId = `${this._id}-tabs-${sheetName}-panel`;
|
|
385
355
|
|
|
386
356
|
const table = new Table(`${this._id}-table-${sheetName}`, {
|
|
387
357
|
striped: this._tableOptions.striped,
|
|
@@ -395,54 +365,59 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
395
365
|
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
396
366
|
table.columns(columnDefs).rows(df.toRows());
|
|
397
367
|
|
|
398
|
-
|
|
399
|
-
|
|
368
|
+
// Add settings button to tab panel
|
|
369
|
+
const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
|
|
370
|
+
label: '⚙️ Import Settings',
|
|
371
|
+
variant: 'ghost',
|
|
372
|
+
size: 'small'
|
|
373
|
+
});
|
|
374
|
+
settingsBtn.bind('click', () => this._showReshapeModal());
|
|
375
|
+
|
|
376
|
+
// Use addTabContent to add components
|
|
377
|
+
this._tabs!.addTabContent(sheetName, [settingsBtn, table]);
|
|
400
378
|
|
|
401
379
|
if (this._tableOptions.filterable) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
380
|
+
// Add filter input above table
|
|
381
|
+
const panel = document.getElementById(panelId);
|
|
382
|
+
if (panel) {
|
|
383
|
+
const filterContainer = document.createElement('div');
|
|
384
|
+
filterContainer.className = 'jux-dataframe-filter';
|
|
385
|
+
|
|
386
|
+
const input = document.createElement('input');
|
|
387
|
+
input.type = 'text';
|
|
388
|
+
input.placeholder = `Filter ${sheetName}...`;
|
|
389
|
+
input.className = 'jux-input-element jux-dataframe-filter-input';
|
|
390
|
+
|
|
391
|
+
const iconEl = renderIcon('search');
|
|
392
|
+
iconEl.style.width = '16px';
|
|
393
|
+
iconEl.style.height = '16px';
|
|
394
|
+
|
|
395
|
+
const iconWrap = document.createElement('span');
|
|
396
|
+
iconWrap.className = 'jux-dataframe-filter-icon';
|
|
397
|
+
iconWrap.appendChild(iconEl);
|
|
398
|
+
|
|
399
|
+
filterContainer.appendChild(iconWrap);
|
|
400
|
+
filterContainer.appendChild(input);
|
|
401
|
+
|
|
402
|
+
input.addEventListener('input', () => {
|
|
403
|
+
const text = input.value.toLowerCase();
|
|
404
|
+
if (!text) { table.rows(df.toRows()); return; }
|
|
405
|
+
const filtered = df.filter((row) =>
|
|
406
|
+
Object.values(row).some(v =>
|
|
407
|
+
v !== null && v !== undefined && String(v).toLowerCase().includes(text)
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
table.rows(filtered.toRows());
|
|
411
|
+
});
|
|
431
412
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
413
|
+
const tableWrapper = panel.querySelector('.jux-table-wrapper');
|
|
414
|
+
if (tableWrapper) {
|
|
415
|
+
panel.insertBefore(filterContainer, tableWrapper);
|
|
416
|
+
}
|
|
435
417
|
}
|
|
436
418
|
}
|
|
437
419
|
});
|
|
438
420
|
|
|
439
|
-
// Ensure Iconify renders the cog icons
|
|
440
|
-
requestAnimationFrame(() => {
|
|
441
|
-
if ((window as any).Iconify) {
|
|
442
|
-
(window as any).Iconify.scan();
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
421
|
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
447
422
|
this._updateStatus(
|
|
448
423
|
`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`,
|
|
@@ -629,7 +604,20 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
629
604
|
|
|
630
605
|
this._cleanupReshapeModal();
|
|
631
606
|
|
|
632
|
-
|
|
607
|
+
// Always detect from a fresh raw parse, not from current _df
|
|
608
|
+
let suggestedRow = 0;
|
|
609
|
+
try {
|
|
610
|
+
const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
611
|
+
headerRow: 0,
|
|
612
|
+
maxSheetSize: 20
|
|
613
|
+
});
|
|
614
|
+
const rawSheet = Object.values(rawSheets)[0];
|
|
615
|
+
if (rawSheet) {
|
|
616
|
+
suggestedRow = this._detectLikelyHeaderRow(rawSheet);
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
suggestedRow = 0;
|
|
620
|
+
}
|
|
633
621
|
|
|
634
622
|
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
635
623
|
title: 'Excel Import Settings',
|
|
@@ -641,7 +629,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
641
629
|
const modalContentHTML = `
|
|
642
630
|
<div style="margin-bottom: 1rem;">
|
|
643
631
|
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
644
|
-
Header Row
|
|
632
|
+
Header Row (0-based index)
|
|
645
633
|
</label>
|
|
646
634
|
<input
|
|
647
635
|
type="number"
|
|
@@ -659,7 +647,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
659
647
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
|
|
660
648
|
Preview
|
|
661
649
|
</div>
|
|
662
|
-
<div id="${this._id}-preview" style="font-family: monospace; font-size:
|
|
650
|
+
<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>
|
|
663
651
|
</div>
|
|
664
652
|
`;
|
|
665
653
|
|
|
@@ -718,9 +706,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
718
706
|
if (!hintDiv) return;
|
|
719
707
|
if (headerRow > 0) {
|
|
720
708
|
hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
|
|
721
|
-
`Rows <strong>0–${headerRow - 1}</strong> will be skipped
|
|
709
|
+
`Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
|
|
722
710
|
} else {
|
|
723
|
-
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers
|
|
711
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
|
|
724
712
|
}
|
|
725
713
|
};
|
|
726
714
|
|
|
@@ -729,10 +717,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
729
717
|
updateHint(headerRow);
|
|
730
718
|
|
|
731
719
|
try {
|
|
732
|
-
//
|
|
720
|
+
// ALWAYS parse with headerRow=0 to get raw file structure
|
|
733
721
|
const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
|
|
734
722
|
headerRow: 0,
|
|
735
|
-
maxSheetSize: headerRow + 12
|
|
723
|
+
maxSheetSize: Math.max(headerRow + 12, 15)
|
|
736
724
|
});
|
|
737
725
|
const rawSheet = Object.values(rawSheets)[0];
|
|
738
726
|
|
|
@@ -741,73 +729,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
741
729
|
return;
|
|
742
730
|
}
|
|
743
731
|
|
|
732
|
+
// rawSheet.columns = row 0 values (when parsed with headerRow=0)
|
|
733
|
+
// rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
|
|
744
734
|
const rawCols = rawSheet.columns;
|
|
745
735
|
const rawRows = rawSheet.toRows();
|
|
746
|
-
const colWidth = 20;
|
|
747
|
-
const lines: string[] = [];
|
|
748
736
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
lines.push('┌─────────────────────────────────────────────────────────────────────────────────┐');
|
|
752
|
-
lines.push(`│ SKIPPED: Rows 0–${headerRow - 1} will NOT be imported │`);
|
|
753
|
-
lines.push('└─────────────────────────────────────────────────────────────────────────────────┘');
|
|
754
|
-
lines.push('');
|
|
755
|
-
|
|
756
|
-
// Row 0 is the raw file header (becomes column names when headerRow=0)
|
|
757
|
-
const row0Line = rawCols.slice(0, 5).map(c => String(c ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
|
|
758
|
-
lines.push(` [0] ${row0Line}${rawCols.length > 5 ? ' ...' : ''}`);
|
|
759
|
-
|
|
760
|
-
// Rows 1 to headerRow-1 are skipped data rows
|
|
761
|
-
for (let i = 0; i < Math.min(headerRow - 1, rawRows.length); i++) {
|
|
762
|
-
const row = rawRows[i];
|
|
763
|
-
const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
|
|
764
|
-
lines.push(` [${i + 1}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
|
|
765
|
-
}
|
|
737
|
+
// Build HTML table showing raw file structure
|
|
738
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
766
739
|
|
|
767
|
-
|
|
768
|
-
|
|
740
|
+
// We need to show rows 0 through headerRow+7 (or so)
|
|
741
|
+
// Row 0 = rawCols, Row 1+ = rawRows[i-1]
|
|
742
|
+
const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
|
|
769
743
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
lines.push('╚═════════════════════════════════════════════════════════════════════════════════╝');
|
|
774
|
-
lines.push('');
|
|
775
|
-
|
|
776
|
-
// The header row content - if headerRow=0, it's rawCols, else it's rawRows[headerRow-1]
|
|
777
|
-
let headerValues: string[];
|
|
778
|
-
if (headerRow === 0) {
|
|
779
|
-
headerValues = rawCols;
|
|
780
|
-
} else if (headerRow - 1 < rawRows.length) {
|
|
781
|
-
headerValues = Object.values(rawRows[headerRow - 1]).map(v => String(v ?? ''));
|
|
782
|
-
} else {
|
|
783
|
-
headerValues = rawCols; // fallback
|
|
784
|
-
}
|
|
744
|
+
for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
|
|
745
|
+
const isHeader = (fileRow === headerRow);
|
|
746
|
+
const isSkipped = (fileRow < headerRow);
|
|
785
747
|
|
|
786
|
-
|
|
787
|
-
lines.push(`▶ [${headerRow}] ${headerLine}${headerValues.length > 5 ? ' ...' : ''}`);
|
|
788
|
-
lines.push('');
|
|
748
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
789
749
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
750
|
+
if (isHeader) {
|
|
751
|
+
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
752
|
+
} else if (isSkipped) {
|
|
753
|
+
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
754
|
+
}
|
|
794
755
|
|
|
795
|
-
|
|
796
|
-
const dataStartIdx = headerRow;
|
|
797
|
-
const dataRows = rawRows.slice(dataStartIdx, dataStartIdx + 6);
|
|
756
|
+
html += `<tr style="${rowStyle}">`;
|
|
798
757
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
758
|
+
// Row index cell
|
|
759
|
+
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;">`;
|
|
760
|
+
if (isHeader) {
|
|
761
|
+
html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
|
|
762
|
+
} else {
|
|
763
|
+
html += `${fileRow}`;
|
|
764
|
+
}
|
|
765
|
+
html += '</td>';
|
|
766
|
+
|
|
767
|
+
// Get values for this file row
|
|
768
|
+
let values: any[];
|
|
769
|
+
if (fileRow === 0) {
|
|
770
|
+
values = rawCols;
|
|
771
|
+
} else {
|
|
772
|
+
values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Show first 6 columns
|
|
776
|
+
const displayCols = values.slice(0, 6);
|
|
777
|
+
displayCols.forEach(val => {
|
|
778
|
+
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
779
|
+
const cellStyle = isHeader
|
|
780
|
+
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
781
|
+
: 'padding: 8px 12px;';
|
|
782
|
+
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
806
783
|
});
|
|
784
|
+
|
|
785
|
+
if (values.length > 6) {
|
|
786
|
+
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Status badge cell
|
|
790
|
+
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
|
|
791
|
+
if (isHeader) {
|
|
792
|
+
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
793
|
+
} else if (isSkipped) {
|
|
794
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
795
|
+
} else {
|
|
796
|
+
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
797
|
+
}
|
|
798
|
+
html += '</td>';
|
|
799
|
+
|
|
800
|
+
html += '</tr>';
|
|
807
801
|
}
|
|
808
802
|
|
|
803
|
+
html += '</table>';
|
|
804
|
+
|
|
809
805
|
if (previewDiv) {
|
|
810
|
-
previewDiv.
|
|
806
|
+
previewDiv.innerHTML = html;
|
|
811
807
|
}
|
|
812
808
|
} catch (err: any) {
|
|
813
809
|
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
@@ -820,6 +816,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
820
816
|
this._reshapeModal.open();
|
|
821
817
|
}
|
|
822
818
|
|
|
819
|
+
private _escapeHtml(text: string): string {
|
|
820
|
+
const div = document.createElement('div');
|
|
821
|
+
div.textContent = text;
|
|
822
|
+
return div.innerHTML;
|
|
823
|
+
}
|
|
824
|
+
|
|
823
825
|
private _showCSVReshapeModal(): void {
|
|
824
826
|
if (!this._rawFileData) return;
|
|
825
827
|
|
|
@@ -843,17 +845,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
843
845
|
</select>
|
|
844
846
|
</div>
|
|
845
847
|
<div style="margin-bottom: 1rem;">
|
|
846
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
|
|
848
|
+
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
|
|
847
849
|
<input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
848
850
|
</div>
|
|
849
|
-
<div style="margin-bottom: 1rem;">
|
|
850
|
-
|
|
851
|
-
<input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
|
|
852
|
-
</div>
|
|
853
|
-
<div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
|
|
854
|
-
<div class="jux-reshape-preview-container" style="margin-top: 1rem;">
|
|
851
|
+
<div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
|
|
852
|
+
<div class="jux-reshape-preview-container">
|
|
855
853
|
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
|
|
856
|
-
<div id="${this._id}-preview"
|
|
854
|
+
<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>
|
|
857
855
|
</div>
|
|
858
856
|
`;
|
|
859
857
|
|
|
@@ -873,11 +871,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
873
871
|
|
|
874
872
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
875
873
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
876
|
-
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
877
874
|
|
|
878
875
|
const delim = delimiterSelect.value;
|
|
879
876
|
const headerRow = parseInt(headerRowInput.value) || 0;
|
|
880
|
-
const skipRows = parseInt(skipRowsInput.value) || 0;
|
|
881
877
|
|
|
882
878
|
this.state.loading = true;
|
|
883
879
|
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
@@ -886,7 +882,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
886
882
|
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
887
883
|
delimiter: delim,
|
|
888
884
|
headerRow,
|
|
889
|
-
skipRows,
|
|
890
885
|
hasHeader: true
|
|
891
886
|
});
|
|
892
887
|
|
|
@@ -907,7 +902,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
907
902
|
requestAnimationFrame(() => {
|
|
908
903
|
const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
|
|
909
904
|
const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
|
|
910
|
-
const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
|
|
911
905
|
const previewDiv = document.getElementById(`${this._id}-preview`)!;
|
|
912
906
|
const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
|
|
913
907
|
|
|
@@ -922,12 +916,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
922
916
|
const updateHint = () => {
|
|
923
917
|
if (!hintDiv) return;
|
|
924
918
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
919
|
+
if (headerRow > 0) {
|
|
920
|
+
hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
|
|
921
|
+
`Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
|
|
922
|
+
} else {
|
|
923
|
+
hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
|
|
924
|
+
}
|
|
931
925
|
};
|
|
932
926
|
|
|
933
927
|
const updatePreview = () => {
|
|
@@ -935,74 +929,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
935
929
|
|
|
936
930
|
const delim = delimiterSelect?.value || ',';
|
|
937
931
|
const headerRow = parseInt(headerRowInput?.value) || 0;
|
|
938
|
-
const skipRows = parseInt(skipRowsInput?.value) || 0;
|
|
939
932
|
|
|
940
933
|
updateHint();
|
|
941
934
|
|
|
942
935
|
try {
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
936
|
+
// Parse raw to show all rows
|
|
937
|
+
const rawDf = this._driver.parseCSV(this._rawFileData.text, {
|
|
938
|
+
delimiter: delim,
|
|
939
|
+
headerRow: 0,
|
|
940
|
+
hasHeader: true,
|
|
941
|
+
maxRows: headerRow + 10
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const rawCols = rawDf.columns;
|
|
945
|
+
const rawRows = rawDf.toRows();
|
|
946
|
+
|
|
947
|
+
// Build HTML table
|
|
948
|
+
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
|
|
949
|
+
|
|
950
|
+
const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
|
|
957
951
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const skippedCount = Math.min(totalOffset - 1, rawRows.length);
|
|
969
|
-
for (let i = 0; i < skippedCount; i++) {
|
|
970
|
-
const row = rawRows[i];
|
|
971
|
-
const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
|
|
972
|
-
const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
|
|
973
|
-
lines.push(`${rowIdx}${cols} ← skipped`);
|
|
952
|
+
for (let i = 0; i < totalRows; i++) {
|
|
953
|
+
const isHeader = (i === headerRow);
|
|
954
|
+
const isSkipped = (i < headerRow);
|
|
955
|
+
|
|
956
|
+
let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
|
|
957
|
+
|
|
958
|
+
if (isHeader) {
|
|
959
|
+
rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
|
|
960
|
+
} else if (isSkipped) {
|
|
961
|
+
rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
|
|
974
962
|
}
|
|
975
963
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
964
|
+
html += `<tr style="${rowStyle}">`;
|
|
965
|
+
|
|
966
|
+
// Row index
|
|
967
|
+
html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
|
|
968
|
+
html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
|
|
969
|
+
html += '</td>';
|
|
970
|
+
|
|
971
|
+
// Data
|
|
972
|
+
let values: any[];
|
|
973
|
+
if (i === 0) {
|
|
974
|
+
values = rawCols;
|
|
975
|
+
} else if (i - 1 < rawRows.length) {
|
|
976
|
+
values = Object.values(rawRows[i - 1]);
|
|
977
|
+
} else {
|
|
978
|
+
values = [];
|
|
979
|
+
}
|
|
982
980
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
skipRows,
|
|
988
|
-
hasHeader: true,
|
|
989
|
-
maxRows: 8
|
|
990
|
-
});
|
|
981
|
+
values.slice(0, 6).forEach(val => {
|
|
982
|
+
const displayVal = val != null ? String(val).substring(0, 25) : '';
|
|
983
|
+
html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
|
|
984
|
+
});
|
|
991
985
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
986
|
+
if (values.length > 6) {
|
|
987
|
+
html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
|
|
988
|
+
}
|
|
995
989
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
990
|
+
// Status
|
|
991
|
+
html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
|
|
992
|
+
if (isHeader) {
|
|
993
|
+
html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
|
|
994
|
+
} else if (isSkipped) {
|
|
995
|
+
html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
|
|
996
|
+
} else {
|
|
997
|
+
html += '<span style="color: hsl(var(--success));">data</span>';
|
|
998
|
+
}
|
|
999
|
+
html += '</td>';
|
|
1002
1000
|
|
|
1003
|
-
|
|
1004
|
-
previewDiv.textContent = lines.join('\n');
|
|
1001
|
+
html += '</tr>';
|
|
1005
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
html += '</table>';
|
|
1005
|
+
|
|
1006
|
+
if (previewDiv) previewDiv.innerHTML = html;
|
|
1006
1007
|
} catch (err: any) {
|
|
1007
1008
|
if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
|
|
1008
1009
|
}
|
|
@@ -1010,7 +1011,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
1010
1011
|
|
|
1011
1012
|
if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
|
|
1012
1013
|
if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
|
|
1013
|
-
if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
|
|
1014
1014
|
|
|
1015
1015
|
updatePreview();
|
|
1016
1016
|
this._reshapeModal!.open();
|