juxscript 1.1.227 → 1.1.230
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/ImportSettingsModal.d.ts +60 -0
- package/lib/components/dataframe/ImportSettingsModal.d.ts.map +1 -0
- package/lib/components/dataframe/ImportSettingsModal.js +442 -0
- package/lib/components/dataframe/ImportSettingsModal.ts +531 -0
- package/lib/components/dataframe.d.ts +2 -99
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +63 -708
- package/lib/components/dataframe.ts +71 -793
- package/lib/styles/shadcn.css +0 -100
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@ import { TabularDriver } from '../storage/TabularDriver.js';
|
|
|
4
4
|
import { FileUpload } from './fileupload.js';
|
|
5
5
|
import { Table } from './table.js';
|
|
6
6
|
import { Tabs } from './tabs.js';
|
|
7
|
-
import { Modal } from './modal.js';
|
|
8
7
|
import { Button } from './button.js';
|
|
9
8
|
import { renderIcon } from './icons.js';
|
|
9
|
+
import { ImportSettingsModal } from './dataframe/ImportSettingsModal.js';
|
|
10
10
|
const TRIGGER_EVENTS = [];
|
|
11
11
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
12
12
|
export class DataFrameComponent extends BaseComponent {
|
|
@@ -30,15 +30,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
30
30
|
this._storageKey = null;
|
|
31
31
|
this._pendingSource = null;
|
|
32
32
|
this._inlineUpload = null;
|
|
33
|
-
this._showStatus = true;
|
|
34
33
|
this._icon = '';
|
|
35
34
|
this._maxSheetSize = 100000;
|
|
36
35
|
this._sheetChunkSize = 10000;
|
|
37
36
|
this._maxFileSize = 50;
|
|
38
|
-
this._showReshapeWarning = true;
|
|
39
37
|
this._rawFileData = null;
|
|
40
|
-
this._reshapeModal = null;
|
|
41
|
-
this._reshapeModalRendered = false;
|
|
42
38
|
this._persistToIndexedDB = false;
|
|
43
39
|
this._clearStorageOnFileRemove = true;
|
|
44
40
|
this._uploadButtonLabel = 'Upload File';
|
|
@@ -47,14 +43,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
47
43
|
this._uploadAccept = '.csv,.tsv,.txt,.xlsx,.xls';
|
|
48
44
|
this._uploadDescription = '';
|
|
49
45
|
this._showUploadIcon = true;
|
|
50
|
-
// ✅ Collapsible state
|
|
51
|
-
this._collapsible = false;
|
|
52
|
-
this._collapsed = false;
|
|
53
|
-
this._summaryTemplate = null;
|
|
54
|
-
this._detailsElement = null;
|
|
55
|
-
this._settingsModal = null;
|
|
56
46
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
57
|
-
this._showStatus = options.showStatus ?? true;
|
|
58
47
|
this._icon = options.icon ?? '';
|
|
59
48
|
this._tableOptions = {
|
|
60
49
|
striped: options.striped ?? true,
|
|
@@ -67,13 +56,13 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
67
56
|
this._maxSheetSize = options.maxSheetSize ?? 100000;
|
|
68
57
|
this._sheetChunkSize = options.sheetChunkSize ?? 10000;
|
|
69
58
|
this._maxFileSize = options.maxFileSize ?? 50;
|
|
70
|
-
this._showReshapeWarning = options.showReshapeWarning ?? true;
|
|
71
59
|
this._persistToIndexedDB = options.persistToIndexedDB ?? false;
|
|
72
60
|
this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
|
|
73
|
-
//
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
// Initialize the import settings modal
|
|
62
|
+
this._importSettingsModal = new ImportSettingsModal(id, this._driver, {
|
|
63
|
+
maxSheetSize: this._maxSheetSize,
|
|
64
|
+
sheetChunkSize: this._sheetChunkSize
|
|
65
|
+
});
|
|
77
66
|
}
|
|
78
67
|
getTriggerEvents() { return TRIGGER_EVENTS; }
|
|
79
68
|
getCallbackEvents() { return CALLBACK_EVENTS; }
|
|
@@ -149,18 +138,12 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
149
138
|
this._uploadButtonIcon = icon;
|
|
150
139
|
return this;
|
|
151
140
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Set upload button label
|
|
154
|
-
*/
|
|
155
141
|
uploadLabel(label) {
|
|
156
142
|
this._uploadButtonLabel = label;
|
|
157
143
|
if (this._inlineUpload)
|
|
158
144
|
this._inlineUpload.label = label;
|
|
159
145
|
return this;
|
|
160
146
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Set upload button icon
|
|
163
|
-
*/
|
|
164
147
|
uploadIcon(icon) {
|
|
165
148
|
this._uploadButtonIcon = icon;
|
|
166
149
|
this._showUploadIcon = !!icon;
|
|
@@ -168,49 +151,31 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
168
151
|
this._inlineUpload.icon = icon;
|
|
169
152
|
return this;
|
|
170
153
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Set upload button variant (outline, primary, ghost, etc.)
|
|
173
|
-
*/
|
|
174
154
|
uploadVariant(variant) {
|
|
175
155
|
this._uploadButtonVariant = variant;
|
|
176
156
|
return this;
|
|
177
157
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Set accepted file types
|
|
180
|
-
*/
|
|
181
158
|
uploadAccept(accept) {
|
|
182
159
|
this._uploadAccept = accept;
|
|
183
160
|
if (this._inlineUpload)
|
|
184
161
|
this._inlineUpload.accept = accept;
|
|
185
162
|
return this;
|
|
186
163
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Set description text below the upload button
|
|
189
|
-
*/
|
|
190
164
|
uploadDescription(description) {
|
|
191
165
|
this._uploadDescription = description;
|
|
192
166
|
return this;
|
|
193
167
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Show/hide upload icon
|
|
196
|
-
*/
|
|
197
168
|
showUploadIcon(show) {
|
|
198
169
|
this._showUploadIcon = show;
|
|
199
170
|
return this;
|
|
200
171
|
}
|
|
201
172
|
/* ═══════════════════════════════════════════════════
|
|
202
|
-
* STORAGE OPTIONS
|
|
173
|
+
* STORAGE OPTIONS
|
|
203
174
|
* ═══════════════════════════════════════════════════ */
|
|
204
|
-
/**
|
|
205
|
-
* Enable/disable IndexedDB persistence (default: false = session only)
|
|
206
|
-
*/
|
|
207
175
|
persistToIndexedDB(enabled) {
|
|
208
176
|
this._persistToIndexedDB = enabled;
|
|
209
177
|
return this;
|
|
210
178
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Clear stored data when file is removed (default: true)
|
|
213
|
-
*/
|
|
214
179
|
clearStorageOnFileRemove(enabled) {
|
|
215
180
|
this._clearStorageOnFileRemove = enabled;
|
|
216
181
|
return this;
|
|
@@ -218,11 +183,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
218
183
|
/* ═══════════════════════════════════════════════════
|
|
219
184
|
* CLEAR / RESET
|
|
220
185
|
* ═══════════════════════════════════════════════════ */
|
|
221
|
-
/**
|
|
222
|
-
* Clear the current data and reset the table
|
|
223
|
-
*/
|
|
224
186
|
async clear() {
|
|
225
|
-
// Clear storage if enabled
|
|
226
187
|
if (this._clearStorageOnFileRemove && this._persistToIndexedDB && this._rawFileData?.file) {
|
|
227
188
|
try {
|
|
228
189
|
const tables = await this._driver.list();
|
|
@@ -235,7 +196,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
235
196
|
console.warn('[DataFrame] Failed to clear storage:', err);
|
|
236
197
|
}
|
|
237
198
|
}
|
|
238
|
-
// Reset state
|
|
239
199
|
this._df = null;
|
|
240
200
|
this._rawFileData = null;
|
|
241
201
|
this._sheets.clear();
|
|
@@ -243,24 +203,16 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
243
203
|
this.state.sourceName = '';
|
|
244
204
|
this.state.rowCount = 0;
|
|
245
205
|
this.state.colCount = 0;
|
|
246
|
-
// Clear table display
|
|
247
206
|
if (this._table) {
|
|
248
207
|
this._table.columns([]).rows([]);
|
|
249
208
|
}
|
|
250
|
-
// Remove tabs if present
|
|
251
209
|
const wrapper = document.getElementById(this._id);
|
|
252
210
|
if (wrapper) {
|
|
253
211
|
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
254
212
|
if (existingTabs)
|
|
255
213
|
existingTabs.remove();
|
|
256
214
|
}
|
|
257
|
-
// ✅ Hide data view, show upload
|
|
258
215
|
this._hideDataView();
|
|
259
|
-
// ✅ Hide collapsible details
|
|
260
|
-
if (this._collapsible && this._detailsElement) {
|
|
261
|
-
this._detailsElement.style.display = 'none';
|
|
262
|
-
}
|
|
263
|
-
// ✅ Clear file from upload component
|
|
264
216
|
if (this._uploadRef) {
|
|
265
217
|
this._uploadRef.clear();
|
|
266
218
|
}
|
|
@@ -269,7 +221,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
269
221
|
/* ═══════════════════════════════════════════════════
|
|
270
222
|
* UI TOGGLES
|
|
271
223
|
* ═══════════════════════════════════════════════════ */
|
|
272
|
-
showStatus(v) { this._showStatus = v; return this; }
|
|
273
224
|
statusIcon(v) { this._icon = v; return this; }
|
|
274
225
|
/* ═══════════════════════════════════════════════════
|
|
275
226
|
* TRANSFORM API
|
|
@@ -321,7 +272,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
321
272
|
return this._driver.store(name, this._df);
|
|
322
273
|
}
|
|
323
274
|
/* ═══════════════════════════════════════════════════
|
|
324
|
-
* TABLE OPTIONS
|
|
275
|
+
* TABLE OPTIONS
|
|
325
276
|
* ═══════════════════════════════════════════════════ */
|
|
326
277
|
striped(v) { this._tableOptions.striped = v; return this; }
|
|
327
278
|
hoverable(v) { this._tableOptions.hoverable = v; return this; }
|
|
@@ -333,76 +284,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
333
284
|
sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
|
|
334
285
|
maxFileSize(mb) { this._maxFileSize = mb; return this; }
|
|
335
286
|
/* ═══════════════════════════════════════════════════
|
|
336
|
-
*
|
|
337
|
-
* ═══════════════════════════════════════════════════ */
|
|
338
|
-
/**
|
|
339
|
-
* Enable/disable collapsible mode
|
|
340
|
-
*/
|
|
341
|
-
collapsible(enabled) {
|
|
342
|
-
this._collapsible = enabled;
|
|
343
|
-
return this;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Set initial collapsed state (true = collapsed, false = expanded)
|
|
347
|
-
*/
|
|
348
|
-
collapsed(value) {
|
|
349
|
-
this._collapsed = value;
|
|
350
|
-
if (this._detailsElement) {
|
|
351
|
-
this._detailsElement.open = !value;
|
|
352
|
-
}
|
|
353
|
-
return this;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Expand the details (show table)
|
|
357
|
-
*/
|
|
358
|
-
expand() {
|
|
359
|
-
this._collapsed = false;
|
|
360
|
-
if (this._detailsElement) {
|
|
361
|
-
this._detailsElement.open = true;
|
|
362
|
-
}
|
|
363
|
-
return this;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Collapse the details (hide table)
|
|
367
|
-
*/
|
|
368
|
-
collapse() {
|
|
369
|
-
this._collapsed = true;
|
|
370
|
-
if (this._detailsElement) {
|
|
371
|
-
this._detailsElement.open = false;
|
|
372
|
-
}
|
|
373
|
-
return this;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Check if collapsible mode is enabled
|
|
377
|
-
*/
|
|
378
|
-
isCollapsible() {
|
|
379
|
-
return this._collapsible;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Toggle collapsed state
|
|
383
|
-
*/
|
|
384
|
-
toggle() {
|
|
385
|
-
this._collapsed = !this._collapsed;
|
|
386
|
-
if (this._detailsElement) {
|
|
387
|
-
this._detailsElement.open = !this._collapsed;
|
|
388
|
-
}
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Set custom summary template
|
|
393
|
-
*/
|
|
394
|
-
summaryTemplate(fn) {
|
|
395
|
-
this._summaryTemplate = fn;
|
|
396
|
-
return this;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Get current collapsed state
|
|
400
|
-
*/
|
|
401
|
-
isCollapsed() {
|
|
402
|
-
return this._collapsed;
|
|
403
|
-
}
|
|
404
|
-
/* ═══════════════════════════════════════════════════
|
|
405
|
-
* FILE HANDLING (modified to use storage options)
|
|
287
|
+
* FILE HANDLING
|
|
406
288
|
* ═══════════════════════════════════════════════════ */
|
|
407
289
|
async _handleFile(file) {
|
|
408
290
|
const fileSizeMB = file.size / (1024 * 1024);
|
|
@@ -431,7 +313,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
431
313
|
this.state.loading = false;
|
|
432
314
|
return;
|
|
433
315
|
}
|
|
434
|
-
// ✅ Only store if persistence is enabled
|
|
435
316
|
if (this._persistToIndexedDB) {
|
|
436
317
|
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
437
318
|
}
|
|
@@ -449,7 +330,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
449
330
|
autoDetectDelimiter: true,
|
|
450
331
|
hasHeader: true
|
|
451
332
|
});
|
|
452
|
-
// ✅ Only store if persistence is enabled
|
|
453
333
|
if (this._persistToIndexedDB) {
|
|
454
334
|
await this._driver.store(file.name, df, { source: file.name });
|
|
455
335
|
}
|
|
@@ -481,7 +361,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
481
361
|
this._sheets.set(name, df);
|
|
482
362
|
});
|
|
483
363
|
const sheetNames = Object.keys(sheets);
|
|
484
|
-
// Sanitize sheet names for use as DOM IDs
|
|
485
364
|
const sanitizeId = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
486
365
|
const tabDefs = sheetNames.map(name => ({
|
|
487
366
|
id: sanitizeId(name),
|
|
@@ -492,26 +371,28 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
492
371
|
tabs: tabDefs,
|
|
493
372
|
activeTab: sanitizeId(sheetNames[0])
|
|
494
373
|
});
|
|
495
|
-
// Map sanitized IDs back to original sheet names
|
|
496
374
|
const idToSheetName = new Map();
|
|
497
375
|
sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
|
|
498
376
|
this._tabs.bind('tabChange', (tabId) => {
|
|
499
377
|
const originalName = idToSheetName.get(tabId) || tabId;
|
|
500
378
|
this._df = this._sheets.get(originalName) || null;
|
|
501
379
|
});
|
|
380
|
+
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
381
|
+
if (dataContainer) {
|
|
382
|
+
dataContainer.style.display = '';
|
|
383
|
+
}
|
|
502
384
|
const tabsContainer = document.createElement('div');
|
|
503
385
|
tabsContainer.className = 'jux-dataframe-tabs';
|
|
504
|
-
|
|
386
|
+
if (dataContainer) {
|
|
387
|
+
dataContainer.appendChild(tabsContainer);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
wrapper.appendChild(tabsContainer);
|
|
391
|
+
}
|
|
505
392
|
this._tabs.render(tabsContainer);
|
|
506
393
|
sheetNames.forEach((sheetName) => {
|
|
507
394
|
const df = sheets[sheetName];
|
|
508
395
|
const safeId = sanitizeId(sheetName);
|
|
509
|
-
// ✅ DEBUG: Log table options
|
|
510
|
-
console.log(`[DataFrame] Creating table for sheet "${sheetName}" with options:`, {
|
|
511
|
-
filterable: this._tableOptions.filterable,
|
|
512
|
-
paginated: this._tableOptions.paginated,
|
|
513
|
-
sortable: this._tableOptions.sortable
|
|
514
|
-
});
|
|
515
396
|
const table = new Table(`${this._id}-table-${safeId}`, {
|
|
516
397
|
striped: this._tableOptions.striped,
|
|
517
398
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -530,8 +411,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
530
411
|
settingsBtn.bind('click', () => this._showReshapeModal());
|
|
531
412
|
this._tabs.addTabContent(safeId, [settingsBtn, table]);
|
|
532
413
|
});
|
|
533
|
-
|
|
534
|
-
this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
|
|
414
|
+
this._showDataView();
|
|
535
415
|
this._df = sheets[sheetNames[0]];
|
|
536
416
|
this._triggerCallback('load', this._df, null, this);
|
|
537
417
|
}
|
|
@@ -542,7 +422,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
542
422
|
const el = document.getElementById(`${this._id}-status`);
|
|
543
423
|
if (!el)
|
|
544
424
|
return;
|
|
545
|
-
// ✅ Only show status during loading/error states
|
|
546
425
|
if (type === 'loading' || type === 'error') {
|
|
547
426
|
el.style.display = '';
|
|
548
427
|
el.className = 'jux-dataframe-status';
|
|
@@ -561,7 +440,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
561
440
|
el.appendChild(span);
|
|
562
441
|
}
|
|
563
442
|
else {
|
|
564
|
-
// ✅ Hide status after successful load - table speaks for itself
|
|
565
443
|
el.style.display = 'none';
|
|
566
444
|
}
|
|
567
445
|
}
|
|
@@ -572,7 +450,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
572
450
|
this.state.sourceName = sourceName;
|
|
573
451
|
this.state.rowCount = df.height;
|
|
574
452
|
this.state.colCount = df.width;
|
|
575
|
-
// Only strip __EMPTY columns that are ENTIRELY null/empty
|
|
576
453
|
const cols = df.columns;
|
|
577
454
|
const rows = df.toRows();
|
|
578
455
|
const emptyColumns = cols.filter(c => {
|
|
@@ -593,58 +470,37 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
593
470
|
const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
|
|
594
471
|
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
595
472
|
}
|
|
596
|
-
// ✅ Show the table container, hide upload area
|
|
597
473
|
this._showDataView();
|
|
598
|
-
// ✅ Update collapsible summary if enabled
|
|
599
|
-
if (this._collapsible && this._detailsElement) {
|
|
600
|
-
this._detailsElement.style.display = '';
|
|
601
|
-
this._updateSummary();
|
|
602
|
-
}
|
|
603
|
-
// ✅ Hide status - data is loaded
|
|
604
474
|
this._updateStatus('', 'success');
|
|
605
475
|
this._triggerCallback('load', this._df, null, this);
|
|
606
476
|
}
|
|
607
|
-
/**
|
|
608
|
-
* Show the data view (table + settings gear), hide upload area
|
|
609
|
-
*/
|
|
610
477
|
_showDataView() {
|
|
611
478
|
const wrapper = document.getElementById(this._id);
|
|
612
479
|
if (!wrapper)
|
|
613
480
|
return;
|
|
614
|
-
// Hide upload area
|
|
615
481
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
|
|
616
482
|
if (uploadArea) {
|
|
617
483
|
uploadArea.style.display = 'none';
|
|
618
484
|
}
|
|
619
|
-
// Show data container (table + toolbar)
|
|
620
485
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
621
486
|
if (dataContainer) {
|
|
622
487
|
dataContainer.style.display = '';
|
|
623
488
|
}
|
|
624
|
-
// Update settings gear with file info
|
|
625
489
|
this._updateSettingsGear();
|
|
626
490
|
}
|
|
627
|
-
/**
|
|
628
|
-
* Hide data view, show upload area
|
|
629
|
-
*/
|
|
630
491
|
_hideDataView() {
|
|
631
492
|
const wrapper = document.getElementById(this._id);
|
|
632
493
|
if (!wrapper)
|
|
633
494
|
return;
|
|
634
|
-
// Show upload area
|
|
635
495
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
|
|
636
496
|
if (uploadArea) {
|
|
637
497
|
uploadArea.style.display = '';
|
|
638
498
|
}
|
|
639
|
-
// Hide data container
|
|
640
499
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
641
500
|
if (dataContainer) {
|
|
642
501
|
dataContainer.style.display = 'none';
|
|
643
502
|
}
|
|
644
503
|
}
|
|
645
|
-
/**
|
|
646
|
-
* Update the settings gear tooltip/info
|
|
647
|
-
*/
|
|
648
504
|
_updateSettingsGear() {
|
|
649
505
|
const gear = document.getElementById(`${this._id}-settings-gear`);
|
|
650
506
|
if (!gear || !this._df)
|
|
@@ -660,159 +516,29 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
660
516
|
}
|
|
661
517
|
}
|
|
662
518
|
/**
|
|
663
|
-
* Show the unified settings modal
|
|
519
|
+
* Show the unified settings modal (now delegated to ImportSettingsModal)
|
|
664
520
|
*/
|
|
665
521
|
_showSettingsModal() {
|
|
666
|
-
this._cleanupReshapeModal();
|
|
667
|
-
const fileInfo = this._rawFileData?.file;
|
|
668
522
|
const isMalformed = this._df ? this._detectMalformedData(this._df) : false;
|
|
669
|
-
this.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
<!-- File Info Section -->
|
|
680
|
-
<div class="jux-dataframe-settings-section">
|
|
681
|
-
<div class="jux-dataframe-settings-label">Source</div>
|
|
682
|
-
<div class="jux-dataframe-settings-value">
|
|
683
|
-
<strong>${this._escapeHtml(fileName)}</strong>
|
|
684
|
-
${fileInfo ? `<span class="jux-muted" style="margin-left: 8px;">${fileSizeKB} KB</span>` : ''}
|
|
685
|
-
</div>
|
|
686
|
-
</div>
|
|
687
|
-
|
|
688
|
-
<!-- Data Info Section -->
|
|
689
|
-
<div class="jux-dataframe-settings-section">
|
|
690
|
-
<div class="jux-dataframe-settings-label">Data</div>
|
|
691
|
-
<div class="jux-dataframe-settings-value">
|
|
692
|
-
${this._df ? `${this._df.height} rows × ${this._df.width} columns` : 'No data loaded'}
|
|
693
|
-
${isMalformed ? '<span style="color: hsl(var(--warning)); margin-left: 8px;">⚠️ May need reformatting</span>' : ''}
|
|
694
|
-
</div>
|
|
695
|
-
</div>
|
|
696
|
-
`;
|
|
697
|
-
// Import Settings button (only if we have raw file data)
|
|
698
|
-
if (this._rawFileData) {
|
|
699
|
-
contentHTML += `
|
|
700
|
-
<div class="jux-dataframe-settings-section">
|
|
701
|
-
<div class="jux-dataframe-settings-label">Import</div>
|
|
702
|
-
<div class="jux-dataframe-settings-value">
|
|
703
|
-
<button id="${this._id}-adjust-import" class="jux-button jux-button-outline jux-button-sm">
|
|
704
|
-
⚙️ Adjust Header Row / Delimiter
|
|
705
|
-
</button>
|
|
706
|
-
</div>
|
|
707
|
-
</div>
|
|
708
|
-
`;
|
|
709
|
-
}
|
|
710
|
-
contentHTML += `
|
|
711
|
-
</div>
|
|
712
|
-
`;
|
|
713
|
-
this._settingsModal
|
|
714
|
-
.content(contentHTML)
|
|
715
|
-
.actions([
|
|
716
|
-
{
|
|
717
|
-
label: 'Remove Data',
|
|
718
|
-
variant: 'secondary',
|
|
719
|
-
click: async () => {
|
|
720
|
-
await this.clear();
|
|
721
|
-
this._settingsModal.closeModal();
|
|
722
|
-
}
|
|
523
|
+
this._importSettingsModal.showSettings({
|
|
524
|
+
fileName: this._rawFileData?.file?.name || this.state.sourceName || 'Unknown',
|
|
525
|
+
fileSize: this._rawFileData?.file?.size,
|
|
526
|
+
rowCount: this._df?.height ?? 0,
|
|
527
|
+
colCount: this._df?.width ?? 0,
|
|
528
|
+
isMalformed,
|
|
529
|
+
hasRawFileData: !!this._rawFileData
|
|
530
|
+
}, {
|
|
531
|
+
onClear: async () => {
|
|
532
|
+
await this.clear();
|
|
723
533
|
},
|
|
724
|
-
{
|
|
725
|
-
|
|
726
|
-
variant: 'primary',
|
|
727
|
-
click: () => this._settingsModal.closeModal()
|
|
728
|
-
}
|
|
729
|
-
]);
|
|
730
|
-
this._settingsModal.render(document.body);
|
|
731
|
-
this._settingsModal.open();
|
|
732
|
-
// Wire up import settings button
|
|
733
|
-
requestAnimationFrame(() => {
|
|
734
|
-
const adjustBtn = document.getElementById(`${this._id}-adjust-import`);
|
|
735
|
-
if (adjustBtn) {
|
|
736
|
-
adjustBtn.addEventListener('click', () => {
|
|
737
|
-
this._settingsModal.closeModal();
|
|
738
|
-
this._showReshapeModal();
|
|
739
|
-
});
|
|
534
|
+
onAdjustImport: () => {
|
|
535
|
+
this._showReshapeModal();
|
|
740
536
|
}
|
|
741
537
|
});
|
|
742
538
|
}
|
|
743
|
-
/**
|
|
744
|
-
* Update the collapsible summary text
|
|
745
|
-
*/
|
|
746
|
-
_updateSummary(isMalformed) {
|
|
747
|
-
if (!this._collapsible || !this._detailsElement)
|
|
748
|
-
return;
|
|
749
|
-
const summaryEl = this._detailsElement.querySelector('.jux-dataframe-summary');
|
|
750
|
-
if (!summaryEl)
|
|
751
|
-
return;
|
|
752
|
-
const malformed = isMalformed ?? (this._df ? this._detectMalformedData(this._df) : false);
|
|
753
|
-
const summaryTextEl = summaryEl.querySelector('.jux-dataframe-summary-text');
|
|
754
|
-
if (summaryTextEl) {
|
|
755
|
-
if (!this._df) {
|
|
756
|
-
summaryTextEl.textContent = 'No data loaded';
|
|
757
|
-
}
|
|
758
|
-
else if (this._summaryTemplate) {
|
|
759
|
-
summaryTextEl.textContent = this._summaryTemplate(this._df);
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
const suffix = malformed ? ' ⚠️' : '';
|
|
763
|
-
summaryTextEl.textContent = `${this.state.sourceName || 'Data'} — ${this._df.height} rows × ${this._df.width} cols${suffix}`;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
// ✅ Add/update settings gear in summary
|
|
767
|
-
let gearBtn = summaryEl.querySelector('.jux-dataframe-summary-gear');
|
|
768
|
-
if (this._df) {
|
|
769
|
-
if (!gearBtn) {
|
|
770
|
-
gearBtn = document.createElement('button');
|
|
771
|
-
gearBtn.className = 'jux-dataframe-summary-gear';
|
|
772
|
-
gearBtn.type = 'button';
|
|
773
|
-
gearBtn.innerHTML = '⚙️';
|
|
774
|
-
gearBtn.addEventListener('click', (e) => {
|
|
775
|
-
e.stopPropagation();
|
|
776
|
-
this._showSettingsModal();
|
|
777
|
-
});
|
|
778
|
-
summaryEl.appendChild(gearBtn);
|
|
779
|
-
}
|
|
780
|
-
if (malformed) {
|
|
781
|
-
gearBtn.classList.add('jux-dataframe-gear-warning');
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
gearBtn.classList.remove('jux-dataframe-gear-warning');
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
else if (gearBtn) {
|
|
788
|
-
gearBtn.remove();
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
539
|
/* ═══════════════════════════════════════════════════
|
|
792
540
|
* MALFORMED DATA DETECTION
|
|
793
541
|
* ═══════════════════════════════════════════════════ */
|
|
794
|
-
_appendSettingsButton(label, variant) {
|
|
795
|
-
const statusEl = document.getElementById(`${this._id}-status`);
|
|
796
|
-
if (!statusEl)
|
|
797
|
-
return;
|
|
798
|
-
// Remove existing settings button if any
|
|
799
|
-
const existing = statusEl.querySelector('.jux-dataframe-settings-btn');
|
|
800
|
-
if (existing)
|
|
801
|
-
existing.remove();
|
|
802
|
-
const btn = document.createElement('button');
|
|
803
|
-
btn.className = `jux-dataframe-settings-btn jux-dataframe-settings-btn-${variant}`;
|
|
804
|
-
btn.type = 'button';
|
|
805
|
-
btn.textContent = `⚙️ ${label}`;
|
|
806
|
-
btn.style.marginLeft = '8px';
|
|
807
|
-
btn.style.cursor = 'pointer';
|
|
808
|
-
btn.style.fontSize = '0.8rem';
|
|
809
|
-
btn.style.padding = '2px 8px';
|
|
810
|
-
btn.style.borderRadius = 'var(--radius, 4px)';
|
|
811
|
-
btn.style.border = '1px solid hsl(var(--border))';
|
|
812
|
-
btn.style.background = variant === 'warning' ? 'hsl(38 92% 50% / 0.15)' : 'transparent';
|
|
813
|
-
btn.addEventListener('click', () => this._showReshapeModal());
|
|
814
|
-
statusEl.appendChild(btn);
|
|
815
|
-
}
|
|
816
542
|
_detectMalformedData(df) {
|
|
817
543
|
const columns = df.columns;
|
|
818
544
|
const rows = df.toRows();
|
|
@@ -836,367 +562,38 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
836
562
|
return false;
|
|
837
563
|
}
|
|
838
564
|
/* ═══════════════════════════════════════════════════
|
|
839
|
-
* RESHAPE MODAL
|
|
565
|
+
* RESHAPE MODAL (delegated to ImportSettingsModal)
|
|
840
566
|
* ═══════════════════════════════════════════════════ */
|
|
841
567
|
_showReshapeModal() {
|
|
842
568
|
if (!this._rawFileData)
|
|
843
569
|
return;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
this._showCSVReshapeModal();
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
_cleanupReshapeModal() {
|
|
852
|
-
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
853
|
-
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
854
|
-
if (oldEl)
|
|
855
|
-
oldEl.remove();
|
|
856
|
-
this._reshapeModal = null;
|
|
857
|
-
this._reshapeModalRendered = false;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
_escapeHtml(text) {
|
|
861
|
-
const div = document.createElement('div');
|
|
862
|
-
div.textContent = text;
|
|
863
|
-
return div.innerHTML;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Build a clickable preview table from raw row data.
|
|
867
|
-
* Each row stores its actual sheet row index via data-sheet-row attribute.
|
|
868
|
-
* Returns the table HTML string.
|
|
869
|
-
*/
|
|
870
|
-
_buildClickablePreviewHTML(rawRows, selectedSheetRow) {
|
|
871
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
872
|
-
for (const { sheetRow, values } of rawRows) {
|
|
873
|
-
const isHeader = (sheetRow === selectedSheetRow);
|
|
874
|
-
const isSkipped = (sheetRow < selectedSheetRow);
|
|
875
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
|
|
876
|
-
if (isHeader) {
|
|
877
|
-
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
878
|
-
}
|
|
879
|
-
else if (isSkipped) {
|
|
880
|
-
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
881
|
-
}
|
|
882
|
-
html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
|
|
883
|
-
// Row index cell
|
|
884
|
-
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;">`;
|
|
885
|
-
if (isHeader) {
|
|
886
|
-
html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
html += `${sheetRow}`;
|
|
890
|
-
}
|
|
891
|
-
html += '</td>';
|
|
892
|
-
// Show first 6 columns
|
|
893
|
-
const displayCols = values.slice(0, 6);
|
|
894
|
-
displayCols.forEach(val => {
|
|
895
|
-
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
896
|
-
const cellStyle = isHeader
|
|
897
|
-
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
898
|
-
: 'padding: 8px 12px;';
|
|
899
|
-
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
900
|
-
});
|
|
901
|
-
if (values.length > 6) {
|
|
902
|
-
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
903
|
-
}
|
|
904
|
-
// Status badge
|
|
905
|
-
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
|
|
906
|
-
if (isHeader) {
|
|
907
|
-
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
908
|
-
}
|
|
909
|
-
else if (isSkipped) {
|
|
910
|
-
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
914
|
-
}
|
|
915
|
-
html += '</td></tr>';
|
|
916
|
-
}
|
|
917
|
-
html += '</table>';
|
|
918
|
-
return html;
|
|
919
|
-
}
|
|
920
|
-
async _showExcelReshapeModal() {
|
|
921
|
-
if (!this._rawFileData?.file)
|
|
922
|
-
return;
|
|
923
|
-
this._cleanupReshapeModal();
|
|
924
|
-
// ✅ Use the SAME cell-reading method as the parser
|
|
925
|
-
const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
|
|
926
|
-
if (rawRows.length === 0)
|
|
927
|
-
return;
|
|
928
|
-
// Log what we got so we can verify alignment
|
|
929
|
-
console.log('[DataFrame Preview] Raw rows from readRawExcelRows:');
|
|
930
|
-
rawRows.forEach(r => console.log(` sheetRow ${r.sheetRow}:`, r.values.slice(0, 5)));
|
|
931
|
-
// Auto-detect best header row
|
|
932
|
-
let selectedSheetRow = rawRows[0].sheetRow;
|
|
933
|
-
for (const { sheetRow, values } of rawRows) {
|
|
934
|
-
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
935
|
-
if (nonEmpty.length < values.length * 0.5)
|
|
936
|
-
continue;
|
|
937
|
-
const nonNumeric = nonEmpty.filter(v => {
|
|
938
|
-
const trimmed = v.trim();
|
|
939
|
-
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
940
|
-
}).length;
|
|
941
|
-
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
942
|
-
selectedSheetRow = sheetRow;
|
|
943
|
-
break;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
947
|
-
title: 'Excel Import Settings',
|
|
948
|
-
size: 'large',
|
|
949
|
-
close: true,
|
|
950
|
-
backdropClose: false
|
|
951
|
-
});
|
|
952
|
-
const modalContentHTML = `
|
|
953
|
-
<div style="margin-bottom: 1rem;">
|
|
954
|
-
<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>
|
|
955
|
-
<input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
|
|
956
|
-
</div>
|
|
957
|
-
<div>
|
|
958
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
959
|
-
<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>
|
|
960
|
-
</div>
|
|
961
|
-
`;
|
|
962
|
-
this._reshapeModal
|
|
963
|
-
.content(modalContentHTML)
|
|
964
|
-
.actions([
|
|
965
|
-
{
|
|
966
|
-
label: 'Cancel',
|
|
967
|
-
variant: 'secondary',
|
|
968
|
-
click: () => this._reshapeModal.closeModal()
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
label: 'Apply & Re-import',
|
|
972
|
-
variant: 'primary',
|
|
973
|
-
click: async () => {
|
|
974
|
-
const input = document.getElementById(`${this._id}-header-row`);
|
|
975
|
-
const headerRow = parseInt(input.value) || 0;
|
|
976
|
-
console.log(`[DataFrame] Apply clicked: headerRow=${headerRow}`);
|
|
977
|
-
this.state.loading = true;
|
|
978
|
-
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
979
|
-
try {
|
|
980
|
-
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
981
|
-
headerRow,
|
|
982
|
-
maxSheetSize: this._maxSheetSize,
|
|
983
|
-
sheetChunkSize: this._sheetChunkSize
|
|
984
|
-
});
|
|
985
|
-
const sheetNames = Object.keys(sheets);
|
|
986
|
-
if (sheetNames.length === 0) {
|
|
987
|
-
this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
|
|
988
|
-
this.state.loading = false;
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
|
|
992
|
-
if (sheetNames.length > 1) {
|
|
993
|
-
this._renderMultiSheet(sheets, this._rawFileData.file.name);
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
|
|
997
|
-
}
|
|
998
|
-
this._reshapeModal.closeModal();
|
|
999
|
-
}
|
|
1000
|
-
catch (err) {
|
|
1001
|
-
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
1002
|
-
this.state.loading = false;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
]);
|
|
1007
|
-
this._reshapeModal.render(document.body);
|
|
1008
|
-
this._reshapeModalRendered = true;
|
|
1009
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
1010
|
-
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
1011
|
-
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
1012
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
1013
|
-
const updateHint = (row) => {
|
|
1014
|
-
if (!hintDiv)
|
|
1015
|
-
return;
|
|
1016
|
-
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
1017
|
-
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
1018
|
-
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
1019
|
-
if (row > rawRows[0].sheetRow) {
|
|
1020
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
const renderPreview = (selected) => {
|
|
1027
|
-
if (!previewDiv)
|
|
1028
|
-
return;
|
|
1029
|
-
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
1030
|
-
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
1031
|
-
tr.addEventListener('click', () => {
|
|
1032
|
-
const rowIdx = parseInt(tr.dataset.sheetRow);
|
|
1033
|
-
hiddenInput.value = String(rowIdx);
|
|
1034
|
-
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
1035
|
-
updateHint(rowIdx);
|
|
1036
|
-
renderPreview(rowIdx);
|
|
1037
|
-
});
|
|
1038
|
-
});
|
|
1039
|
-
};
|
|
1040
|
-
updateHint(selectedSheetRow);
|
|
1041
|
-
renderPreview(selectedSheetRow);
|
|
1042
|
-
this._reshapeModal.open();
|
|
1043
|
-
}
|
|
1044
|
-
_showCSVReshapeModal() {
|
|
1045
|
-
if (!this._rawFileData?.text)
|
|
1046
|
-
return;
|
|
1047
|
-
this._cleanupReshapeModal();
|
|
1048
|
-
const text = this._rawFileData.text;
|
|
1049
|
-
const detected = this._driver._detectDelimiter(text);
|
|
1050
|
-
// Parse raw lines
|
|
1051
|
-
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
1052
|
-
const rawRows = [];
|
|
1053
|
-
const maxPreviewRows = Math.min(lines.length, 15);
|
|
1054
|
-
for (let i = 0; i < maxPreviewRows; i++) {
|
|
1055
|
-
if (!lines[i]) {
|
|
1056
|
-
rawRows.push({ sheetRow: i, values: [''] });
|
|
1057
|
-
continue;
|
|
1058
|
-
}
|
|
1059
|
-
const values = this._driver._parseLine(lines[i], detected);
|
|
1060
|
-
rawRows.push({ sheetRow: i, values });
|
|
1061
|
-
}
|
|
1062
|
-
// Auto-detect header row
|
|
1063
|
-
let selectedRow = 0;
|
|
1064
|
-
for (const { sheetRow, values } of rawRows) {
|
|
1065
|
-
const nonEmpty = values.filter((v) => v.trim() !== '');
|
|
1066
|
-
if (nonEmpty.length < values.length * 0.5)
|
|
1067
|
-
continue;
|
|
1068
|
-
const nonNumeric = nonEmpty.filter((v) => {
|
|
1069
|
-
const trimmed = v.trim();
|
|
1070
|
-
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
1071
|
-
}).length;
|
|
1072
|
-
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
1073
|
-
selectedRow = sheetRow;
|
|
1074
|
-
break;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
1078
|
-
title: 'CSV Import Settings',
|
|
1079
|
-
size: 'large',
|
|
1080
|
-
close: true,
|
|
1081
|
-
backdropClose: false
|
|
1082
|
-
});
|
|
1083
|
-
const modalContentHTML = `
|
|
1084
|
-
<div style="margin-bottom: 1rem;">
|
|
1085
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
1086
|
-
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
1087
|
-
<option value=",">Comma (,)</option>
|
|
1088
|
-
<option value="|">Pipe (|)</option>
|
|
1089
|
-
<option value="	">Tab (\\t)</option>
|
|
1090
|
-
<option value=";">Semicolon (;)</option>
|
|
1091
|
-
</select>
|
|
1092
|
-
</div>
|
|
1093
|
-
<div style="margin-bottom: 1rem;">
|
|
1094
|
-
<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>
|
|
1095
|
-
<input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
|
|
1096
|
-
</div>
|
|
1097
|
-
<div>
|
|
1098
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
1099
|
-
<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>
|
|
1100
|
-
</div>
|
|
1101
|
-
`;
|
|
1102
|
-
this._reshapeModal
|
|
1103
|
-
.content(modalContentHTML)
|
|
1104
|
-
.actions([
|
|
1105
|
-
{
|
|
1106
|
-
label: 'Cancel',
|
|
1107
|
-
variant: 'secondary',
|
|
1108
|
-
click: () => this._reshapeModal.closeModal()
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
label: 'Apply & Re-import',
|
|
1112
|
-
variant: 'primary',
|
|
1113
|
-
click: async () => {
|
|
1114
|
-
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
1115
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
1116
|
-
const delim = delimiterSelect.value;
|
|
1117
|
-
const headerRow = parseInt(hiddenInput.value) || 0;
|
|
1118
|
-
this.state.loading = true;
|
|
1119
|
-
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
1120
|
-
try {
|
|
1121
|
-
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
1122
|
-
delimiter: delim,
|
|
1123
|
-
headerRow,
|
|
1124
|
-
hasHeader: true
|
|
1125
|
-
});
|
|
1126
|
-
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
1127
|
-
this._setDataFrame(df, this._rawFileData.file.name);
|
|
1128
|
-
this._reshapeModal.closeModal();
|
|
1129
|
-
}
|
|
1130
|
-
catch (err) {
|
|
1131
|
-
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
1132
|
-
this.state.loading = false;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
]);
|
|
1137
|
-
this._reshapeModal.render(document.body);
|
|
1138
|
-
this._reshapeModalRendered = true;
|
|
1139
|
-
requestAnimationFrame(() => {
|
|
1140
|
-
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
1141
|
-
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
1142
|
-
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
1143
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
1144
|
-
if (delimiterSelect)
|
|
1145
|
-
delimiterSelect.value = detected;
|
|
1146
|
-
const updateHint = (row) => {
|
|
1147
|
-
if (!hintDiv)
|
|
1148
|
-
return;
|
|
1149
|
-
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
1150
|
-
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
1151
|
-
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
1152
|
-
if (row > rawRows[0].sheetRow) {
|
|
1153
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
570
|
+
this._importSettingsModal.showImportSettings(this._rawFileData, {
|
|
571
|
+
onReimport: (result, sourceName) => {
|
|
572
|
+
if (result instanceof DataFrame) {
|
|
573
|
+
this._setDataFrame(result, sourceName);
|
|
1154
574
|
}
|
|
1155
575
|
else {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
for (let i = 0; i < maxPreviewRows; i++) {
|
|
1163
|
-
if (!lines[i]) {
|
|
1164
|
-
rawRows.push({ sheetRow: i, values: [''] });
|
|
1165
|
-
continue;
|
|
576
|
+
const sheetNames = Object.keys(result);
|
|
577
|
+
if (sheetNames.length > 1) {
|
|
578
|
+
this._renderMultiSheet(result, sourceName);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
this._setDataFrame(result[sheetNames[0]], sourceName);
|
|
1166
582
|
}
|
|
1167
|
-
const values = this._driver._parseLine(lines[i], delim);
|
|
1168
|
-
rawRows.push({ sheetRow: i, values });
|
|
1169
583
|
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
hiddenInput.value = String(rowIdx);
|
|
1179
|
-
console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
|
|
1180
|
-
updateHint(rowIdx);
|
|
1181
|
-
renderPreview(rowIdx);
|
|
1182
|
-
});
|
|
1183
|
-
});
|
|
1184
|
-
};
|
|
1185
|
-
if (delimiterSelect) {
|
|
1186
|
-
delimiterSelect.addEventListener('change', () => {
|
|
1187
|
-
reparse();
|
|
1188
|
-
const current = parseInt(hiddenInput.value) || 0;
|
|
1189
|
-
updateHint(current);
|
|
1190
|
-
renderPreview(current);
|
|
1191
|
-
});
|
|
584
|
+
},
|
|
585
|
+
onError: (message) => {
|
|
586
|
+
this._updateStatus(message, 'error');
|
|
587
|
+
this.state.loading = false;
|
|
588
|
+
},
|
|
589
|
+
onStatusUpdate: (text, type) => {
|
|
590
|
+
this.state.loading = type === 'loading';
|
|
591
|
+
this._updateStatus(text, type);
|
|
1192
592
|
}
|
|
1193
|
-
updateHint(selectedRow);
|
|
1194
|
-
renderPreview(selectedRow);
|
|
1195
|
-
this._reshapeModal.open();
|
|
1196
593
|
});
|
|
1197
594
|
}
|
|
1198
595
|
/* ═══════════════════════════════════════════════════
|
|
1199
|
-
* UPDATE & RENDER
|
|
596
|
+
* UPDATE & RENDER
|
|
1200
597
|
* ═══════════════════════════════════════════════════ */
|
|
1201
598
|
update(_prop, _value) { }
|
|
1202
599
|
render(targetId) {
|
|
@@ -1209,9 +606,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1209
606
|
wrapper.className += ` ${className}`;
|
|
1210
607
|
if (style)
|
|
1211
608
|
wrapper.setAttribute('style', style);
|
|
1212
|
-
//
|
|
1213
|
-
// UPLOAD AREA (shown initially, hidden after data loads)
|
|
1214
|
-
// ═══════════════════════════════════════════════════
|
|
609
|
+
// Upload area
|
|
1215
610
|
if (this._inlineUpload) {
|
|
1216
611
|
const uploadArea = document.createElement('div');
|
|
1217
612
|
uploadArea.className = 'jux-dataframe-upload-area';
|
|
@@ -1239,14 +634,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1239
634
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
1240
635
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
1241
636
|
uploadArea.appendChild(uploadContainer);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
statusBar.style.display = 'none';
|
|
1248
|
-
uploadArea.appendChild(statusBar);
|
|
1249
|
-
}
|
|
637
|
+
const statusBar = document.createElement('div');
|
|
638
|
+
statusBar.className = 'jux-dataframe-status';
|
|
639
|
+
statusBar.id = `${this._id}-status`;
|
|
640
|
+
statusBar.style.display = 'none';
|
|
641
|
+
uploadArea.appendChild(statusBar);
|
|
1250
642
|
if (this._uploadDescription) {
|
|
1251
643
|
const descEl = document.createElement('div');
|
|
1252
644
|
descEl.className = 'jux-dataframe-upload-description';
|
|
@@ -1260,13 +652,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1260
652
|
else {
|
|
1261
653
|
container.appendChild(wrapper);
|
|
1262
654
|
}
|
|
1263
|
-
//
|
|
1264
|
-
// DATA CONTAINER (hidden initially, shown after data loads)
|
|
1265
|
-
// ═══════════════════════════════════════════════════
|
|
655
|
+
// Data container
|
|
1266
656
|
const dataContainer = document.createElement('div');
|
|
1267
657
|
dataContainer.className = 'jux-dataframe-data';
|
|
1268
|
-
dataContainer.style.display = 'none';
|
|
1269
|
-
// ✅ Toolbar with settings gear
|
|
658
|
+
dataContainer.style.display = 'none';
|
|
1270
659
|
const toolbar = document.createElement('div');
|
|
1271
660
|
toolbar.className = 'jux-dataframe-toolbar';
|
|
1272
661
|
const settingsGear = document.createElement('button');
|
|
@@ -1278,42 +667,8 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1278
667
|
settingsGear.addEventListener('click', () => this._showSettingsModal());
|
|
1279
668
|
toolbar.appendChild(settingsGear);
|
|
1280
669
|
dataContainer.appendChild(toolbar);
|
|
1281
|
-
// ═══════════════════════════════════════════════════
|
|
1282
|
-
// TABLE CONTAINER (inside data container or collapsible)
|
|
1283
|
-
// ═══════════════════════════════════════════════════
|
|
1284
|
-
let tableContainer;
|
|
1285
|
-
if (this._collapsible) {
|
|
1286
|
-
const details = document.createElement('details');
|
|
1287
|
-
details.className = 'jux-dataframe-details';
|
|
1288
|
-
details.open = !this._collapsed;
|
|
1289
|
-
details.style.display = 'none'; // Hidden until data loads
|
|
1290
|
-
this._detailsElement = details;
|
|
1291
|
-
const summary = document.createElement('summary');
|
|
1292
|
-
summary.className = 'jux-dataframe-summary';
|
|
1293
|
-
const chevron = document.createElement('span');
|
|
1294
|
-
chevron.className = 'jux-dataframe-summary-chevron';
|
|
1295
|
-
chevron.innerHTML = '▶';
|
|
1296
|
-
summary.appendChild(chevron);
|
|
1297
|
-
const textSpan = document.createElement('span');
|
|
1298
|
-
textSpan.className = 'jux-dataframe-summary-text';
|
|
1299
|
-
summary.appendChild(textSpan);
|
|
1300
|
-
details.appendChild(summary);
|
|
1301
|
-
const content = document.createElement('div');
|
|
1302
|
-
content.className = 'jux-dataframe-details-content';
|
|
1303
|
-
details.appendChild(content);
|
|
1304
|
-
dataContainer.appendChild(details);
|
|
1305
|
-
tableContainer = content;
|
|
1306
|
-
details.addEventListener('toggle', () => {
|
|
1307
|
-
this._collapsed = !details.open;
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
else {
|
|
1311
|
-
tableContainer = dataContainer;
|
|
1312
|
-
}
|
|
1313
670
|
wrapper.appendChild(dataContainer);
|
|
1314
|
-
//
|
|
1315
|
-
// TABLE
|
|
1316
|
-
// ═══════════════════════════════════════════════════
|
|
671
|
+
// Table
|
|
1317
672
|
const tbl = new Table(`${this._id}-table`, {
|
|
1318
673
|
striped: this._tableOptions.striped,
|
|
1319
674
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -1322,7 +677,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1322
677
|
paginated: this._tableOptions.paginated,
|
|
1323
678
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
1324
679
|
});
|
|
1325
|
-
tbl.render(
|
|
680
|
+
tbl.render(dataContainer);
|
|
1326
681
|
this._table = tbl;
|
|
1327
682
|
if (this._pendingSource) {
|
|
1328
683
|
const fn = this._pendingSource;
|