juxscript 1.1.228 → 1.1.231
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/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/lib/components/dataframe/DataFrameSource.d.ts +118 -0
- package/lib/components/dataframe/DataFrameSource.d.ts.map +1 -0
- package/lib/components/dataframe/DataFrameSource.js +421 -0
- package/lib/components/dataframe/DataFrameSource.ts +532 -0
- 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 +46 -61
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +189 -780
- package/lib/components/dataframe.ts +205 -886
- package/package.json +1 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
2
|
import { DataFrame } from '../storage/DataFrame.js';
|
|
3
|
-
import { TabularDriver } from '../storage/TabularDriver.js';
|
|
4
3
|
import { FileUpload } from './fileupload.js';
|
|
5
4
|
import { Table } from './table.js';
|
|
6
5
|
import { Tabs } from './tabs.js';
|
|
7
|
-
import { Modal } from './modal.js';
|
|
8
6
|
import { Button } from './button.js';
|
|
9
|
-
import {
|
|
7
|
+
import { dataFrameSource } from './dataframe/DataFrameSource.js';
|
|
8
|
+
import { ImportSettingsModal } from './dataframe/ImportSettingsModal.js';
|
|
10
9
|
const TRIGGER_EVENTS = [];
|
|
11
10
|
const CALLBACK_EVENTS = ['load', 'error', 'transform'];
|
|
12
11
|
export class DataFrameComponent extends BaseComponent {
|
|
@@ -22,35 +21,35 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
22
21
|
rowCount: 0,
|
|
23
22
|
colCount: 0
|
|
24
23
|
});
|
|
25
|
-
this._df = null;
|
|
26
24
|
this._table = null;
|
|
27
25
|
this._tabs = null;
|
|
28
|
-
this._sheets = new Map();
|
|
29
26
|
this._uploadRef = null;
|
|
30
|
-
this._storageKey = null;
|
|
31
|
-
this._pendingSource = null;
|
|
32
27
|
this._inlineUpload = null;
|
|
33
|
-
this._showStatus = true;
|
|
34
|
-
this._icon = '';
|
|
35
|
-
this._maxSheetSize = 100000;
|
|
36
|
-
this._sheetChunkSize = 10000;
|
|
37
|
-
this._maxFileSize = 50;
|
|
38
|
-
this._showReshapeWarning = true;
|
|
39
|
-
this._rawFileData = null;
|
|
40
|
-
this._reshapeModal = null;
|
|
41
|
-
this._reshapeModalRendered = false;
|
|
42
|
-
this._persistToIndexedDB = false;
|
|
43
|
-
this._clearStorageOnFileRemove = true;
|
|
44
28
|
this._uploadButtonLabel = 'Upload File';
|
|
45
29
|
this._uploadButtonIcon = 'upload';
|
|
46
30
|
this._uploadButtonVariant = 'outline';
|
|
47
31
|
this._uploadAccept = '.csv,.tsv,.txt,.xlsx,.xls';
|
|
48
32
|
this._uploadDescription = '';
|
|
49
33
|
this._showUploadIcon = true;
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
34
|
+
this._pendingSource = null;
|
|
35
|
+
// Initialize data source
|
|
36
|
+
this._source = dataFrameSource({
|
|
37
|
+
maxFileSize: options.maxFileSize ?? 50,
|
|
38
|
+
maxSheetSize: options.maxSheetSize ?? 100000,
|
|
39
|
+
sheetChunkSize: options.sheetChunkSize ?? 10000,
|
|
40
|
+
persistToIndexedDB: options.persistToIndexedDB ?? false,
|
|
41
|
+
dbName: options.dbName ?? 'jux-dataframes',
|
|
42
|
+
storeName: options.storeName ?? 'frames'
|
|
43
|
+
});
|
|
44
|
+
// Wire up source callbacks
|
|
45
|
+
this._source.onLoad((result) => this._handleLoadResult(result));
|
|
46
|
+
this._source.onError((message) => this._handleError(message));
|
|
47
|
+
this._source.onProgress((loaded, total) => this._handleProgress(loaded, total));
|
|
48
|
+
// Initialize import settings modal
|
|
49
|
+
this._importModal = new ImportSettingsModal(id, this._source.driver, {
|
|
50
|
+
maxSheetSize: options.maxSheetSize ?? 100000,
|
|
51
|
+
sheetChunkSize: options.sheetChunkSize ?? 10000
|
|
52
|
+
});
|
|
54
53
|
this._tableOptions = {
|
|
55
54
|
striped: options.striped ?? true,
|
|
56
55
|
hoverable: options.hoverable ?? true,
|
|
@@ -59,38 +58,20 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
59
58
|
paginated: options.paginated ?? true,
|
|
60
59
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
61
60
|
};
|
|
62
|
-
this._maxSheetSize = options.maxSheetSize ?? 100000;
|
|
63
|
-
this._sheetChunkSize = options.sheetChunkSize ?? 10000;
|
|
64
|
-
this._maxFileSize = options.maxFileSize ?? 50;
|
|
65
|
-
this._showReshapeWarning = options.showReshapeWarning ?? true;
|
|
66
|
-
this._persistToIndexedDB = options.persistToIndexedDB ?? false;
|
|
67
|
-
this._clearStorageOnFileRemove = options.clearStorageOnFileRemove ?? true;
|
|
68
61
|
}
|
|
69
62
|
getTriggerEvents() { return TRIGGER_EVENTS; }
|
|
70
63
|
getCallbackEvents() { return CALLBACK_EVENTS; }
|
|
71
64
|
/* ═══════════════════════════════════════════════════
|
|
72
|
-
* DATA
|
|
65
|
+
* DATA SOURCE METHODS (Delegate to DataFrameSource)
|
|
73
66
|
* ═══════════════════════════════════════════════════ */
|
|
67
|
+
/**
|
|
68
|
+
* Load from IndexedDB storage
|
|
69
|
+
*/
|
|
74
70
|
fromStorage(key) {
|
|
75
|
-
this._storageKey = key;
|
|
76
71
|
const loadFn = async () => {
|
|
77
72
|
this.state.loading = true;
|
|
78
73
|
this._updateStatus('Loading...', 'loading');
|
|
79
|
-
|
|
80
|
-
const df = await this._driver.loadByName(key);
|
|
81
|
-
if (!df) {
|
|
82
|
-
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
83
|
-
this.state.loading = false;
|
|
84
|
-
this._updateStatus('No table found: ' + key, 'empty');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
this._setDataFrame(df, 'storage: ' + key);
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
this._triggerCallback('error', err.message, null, this);
|
|
91
|
-
this.state.loading = false;
|
|
92
|
-
this._updateStatus(err.message, 'error');
|
|
93
|
-
}
|
|
74
|
+
await this._source.fromStorage(key);
|
|
94
75
|
};
|
|
95
76
|
if (this._table) {
|
|
96
77
|
loadFn();
|
|
@@ -100,30 +81,24 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
100
81
|
}
|
|
101
82
|
return this;
|
|
102
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Load from a FileUpload component
|
|
86
|
+
*/
|
|
103
87
|
fromUpload(upload) {
|
|
104
88
|
this._uploadRef = upload;
|
|
105
89
|
this._pendingSource = async () => {
|
|
106
|
-
|
|
107
|
-
if (!files || files.length === 0)
|
|
108
|
-
return;
|
|
109
|
-
await this._handleFile(files[0]);
|
|
110
|
-
});
|
|
90
|
+
this._source.fromUpload(upload);
|
|
111
91
|
};
|
|
112
92
|
return this;
|
|
113
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Load from inline data
|
|
96
|
+
*/
|
|
114
97
|
fromData(data) {
|
|
115
98
|
const loadFn = async () => {
|
|
116
99
|
this.state.loading = true;
|
|
117
100
|
this._updateStatus('Loading data...', 'loading');
|
|
118
|
-
|
|
119
|
-
const df = new DataFrame(data);
|
|
120
|
-
this._setDataFrame(df, 'inline data');
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
this._triggerCallback('error', err.message, null, this);
|
|
124
|
-
this.state.loading = false;
|
|
125
|
-
this._updateStatus(err.message, 'error');
|
|
126
|
-
}
|
|
101
|
+
this._source.fromData(data);
|
|
127
102
|
};
|
|
128
103
|
if (this._table) {
|
|
129
104
|
loadFn();
|
|
@@ -133,6 +108,26 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
133
108
|
}
|
|
134
109
|
return this;
|
|
135
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Load from a URL
|
|
113
|
+
*/
|
|
114
|
+
fromUrl(url) {
|
|
115
|
+
const loadFn = async () => {
|
|
116
|
+
this.state.loading = true;
|
|
117
|
+
this._updateStatus('Fetching...', 'loading');
|
|
118
|
+
await this._source.fromUrl(url);
|
|
119
|
+
};
|
|
120
|
+
if (this._table) {
|
|
121
|
+
loadFn();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this._pendingSource = loadFn;
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Enable inline file upload UI
|
|
130
|
+
*/
|
|
136
131
|
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
|
|
137
132
|
this._inlineUpload = { label, accept, icon };
|
|
138
133
|
this._uploadButtonLabel = label;
|
|
@@ -140,99 +135,47 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
140
135
|
this._uploadButtonIcon = icon;
|
|
141
136
|
return this;
|
|
142
137
|
}
|
|
143
|
-
uploadLabel(label) {
|
|
144
|
-
this._uploadButtonLabel = label;
|
|
145
|
-
if (this._inlineUpload)
|
|
146
|
-
this._inlineUpload.label = label;
|
|
147
|
-
return this;
|
|
148
|
-
}
|
|
149
|
-
uploadIcon(icon) {
|
|
150
|
-
this._uploadButtonIcon = icon;
|
|
151
|
-
this._showUploadIcon = !!icon;
|
|
152
|
-
if (this._inlineUpload)
|
|
153
|
-
this._inlineUpload.icon = icon;
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
uploadVariant(variant) {
|
|
157
|
-
this._uploadButtonVariant = variant;
|
|
158
|
-
return this;
|
|
159
|
-
}
|
|
160
|
-
uploadAccept(accept) {
|
|
161
|
-
this._uploadAccept = accept;
|
|
162
|
-
if (this._inlineUpload)
|
|
163
|
-
this._inlineUpload.accept = accept;
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
uploadDescription(description) {
|
|
167
|
-
this._uploadDescription = description;
|
|
168
|
-
return this;
|
|
169
|
-
}
|
|
170
|
-
showUploadIcon(show) {
|
|
171
|
-
this._showUploadIcon = show;
|
|
172
|
-
return this;
|
|
173
|
-
}
|
|
174
138
|
/* ═══════════════════════════════════════════════════
|
|
175
|
-
*
|
|
139
|
+
* UPLOAD UI CONFIGURATION
|
|
176
140
|
* ═══════════════════════════════════════════════════ */
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
141
|
+
uploadLabel(label) { this._uploadButtonLabel = label; return this; }
|
|
142
|
+
uploadIcon(icon) { this._uploadButtonIcon = icon; this._showUploadIcon = !!icon; return this; }
|
|
143
|
+
uploadVariant(variant) { this._uploadButtonVariant = variant; return this; }
|
|
144
|
+
uploadAccept(accept) { this._uploadAccept = accept; return this; }
|
|
145
|
+
uploadDescription(description) { this._uploadDescription = description; return this; }
|
|
146
|
+
showUploadIcon(show) { this._showUploadIcon = show; return this; }
|
|
185
147
|
/* ═══════════════════════════════════════════════════
|
|
186
|
-
*
|
|
148
|
+
* TABLE OPTIONS
|
|
187
149
|
* ═══════════════════════════════════════════════════ */
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await this._driver.delete(table.id);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
console.warn('[DataFrame] Failed to clear storage:', err);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
this._df = null;
|
|
202
|
-
this._rawFileData = null;
|
|
203
|
-
this._sheets.clear();
|
|
204
|
-
this.state.loaded = false;
|
|
205
|
-
this.state.sourceName = '';
|
|
206
|
-
this.state.rowCount = 0;
|
|
207
|
-
this.state.colCount = 0;
|
|
208
|
-
if (this._table) {
|
|
209
|
-
this._table.columns([]).rows([]);
|
|
210
|
-
}
|
|
211
|
-
const wrapper = document.getElementById(this._id);
|
|
212
|
-
if (wrapper) {
|
|
213
|
-
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
214
|
-
if (existingTabs)
|
|
215
|
-
existingTabs.remove();
|
|
216
|
-
}
|
|
217
|
-
this._hideDataView();
|
|
218
|
-
if (this._uploadRef) {
|
|
219
|
-
this._uploadRef.clear();
|
|
220
|
-
}
|
|
221
|
-
return this;
|
|
222
|
-
}
|
|
150
|
+
striped(v) { this._tableOptions.striped = v; return this; }
|
|
151
|
+
hoverable(v) { this._tableOptions.hoverable = v; return this; }
|
|
152
|
+
sortable(v) { this._tableOptions.sortable = v; return this; }
|
|
153
|
+
filterable(v) { this._tableOptions.filterable = v; return this; }
|
|
154
|
+
paginated(v) { this._tableOptions.paginated = v; return this; }
|
|
155
|
+
rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
|
|
223
156
|
/* ═══════════════════════════════════════════════════
|
|
224
|
-
*
|
|
157
|
+
* ACCESSORS (Delegate to DataFrameSource)
|
|
225
158
|
* ═══════════════════════════════════════════════════ */
|
|
226
|
-
|
|
227
|
-
|
|
159
|
+
get df() { return this._source.df; }
|
|
160
|
+
get source() { return this._source; }
|
|
161
|
+
get table() { return this._table; }
|
|
162
|
+
get shape() { return this._source.shape; }
|
|
163
|
+
get columns() { return this._source.columns; }
|
|
164
|
+
toRows() { return this._source.df?.toRows() ?? []; }
|
|
165
|
+
toCSV(delimiter) { return this._source.df?.toCSV(delimiter) ?? ''; }
|
|
166
|
+
describe() { return this._source.df?.describe() ?? null; }
|
|
167
|
+
async save(key) {
|
|
168
|
+
return this._source.save(key);
|
|
169
|
+
}
|
|
228
170
|
/* ═══════════════════════════════════════════════════
|
|
229
|
-
* TRANSFORM API
|
|
171
|
+
* TRANSFORM API (Operate on underlying DataFrame)
|
|
230
172
|
* ═══════════════════════════════════════════════════ */
|
|
231
173
|
apply(fn) {
|
|
232
|
-
|
|
174
|
+
const df = this._source.df;
|
|
175
|
+
if (!df)
|
|
233
176
|
return this;
|
|
234
|
-
const result = fn(
|
|
235
|
-
this.
|
|
177
|
+
const result = fn(df);
|
|
178
|
+
this._source.fromData(result.toRows(), this.state.sourceName + ' (transformed)');
|
|
236
179
|
this._triggerCallback('transform', result, null, this);
|
|
237
180
|
return this;
|
|
238
181
|
}
|
|
@@ -251,118 +194,69 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
251
194
|
tail(n = 5) {
|
|
252
195
|
return this.apply(df => df.tail(n));
|
|
253
196
|
}
|
|
254
|
-
withColumn(name, fn) {
|
|
255
|
-
return this.apply(df => df.withColumn(name, fn));
|
|
256
|
-
}
|
|
257
|
-
where(col, op, value) {
|
|
258
|
-
return this.apply(df => df.where(col, op, value));
|
|
259
|
-
}
|
|
260
197
|
/* ═══════════════════════════════════════════════════
|
|
261
|
-
*
|
|
198
|
+
* CLEAR / RESET
|
|
262
199
|
* ═══════════════════════════════════════════════════ */
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return this
|
|
200
|
+
async clear() {
|
|
201
|
+
await this._source.clearStorage();
|
|
202
|
+
this._source.clear();
|
|
203
|
+
this.state.loaded = false;
|
|
204
|
+
this.state.sourceName = '';
|
|
205
|
+
this.state.rowCount = 0;
|
|
206
|
+
this.state.colCount = 0;
|
|
207
|
+
if (this._table) {
|
|
208
|
+
this._table.columns([]).rows([]);
|
|
209
|
+
}
|
|
210
|
+
this._hideDataView();
|
|
211
|
+
this._uploadRef?.clear();
|
|
212
|
+
return this;
|
|
276
213
|
}
|
|
277
214
|
/* ═══════════════════════════════════════════════════
|
|
278
|
-
*
|
|
279
|
-
* ═══════════════════════════════════════════════════ */
|
|
280
|
-
striped(v) { this._tableOptions.striped = v; return this; }
|
|
281
|
-
hoverable(v) { this._tableOptions.hoverable = v; return this; }
|
|
282
|
-
sortable(v) { this._tableOptions.sortable = v; return this; }
|
|
283
|
-
filterable(v) { this._tableOptions.filterable = v; return this; }
|
|
284
|
-
paginated(v) { this._tableOptions.paginated = v; return this; }
|
|
285
|
-
rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
|
|
286
|
-
maxSheetSize(v) { this._maxSheetSize = v; return this; }
|
|
287
|
-
sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
|
|
288
|
-
maxFileSize(mb) { this._maxFileSize = mb; return this; }
|
|
289
|
-
/* ═══════════════════════════════════════════════════
|
|
290
|
-
* FILE HANDLING
|
|
215
|
+
* INTERNAL HANDLERS
|
|
291
216
|
* ═══════════════════════════════════════════════════ */
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this._updateStatus(`File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
|
|
217
|
+
_handleLoadResult(result) {
|
|
218
|
+
if (result.error) {
|
|
219
|
+
this._handleError(result.error);
|
|
296
220
|
return;
|
|
297
221
|
}
|
|
298
|
-
this.state.loading =
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const sheets = await this._driver.streamFileMultiSheet(file, {
|
|
306
|
-
maxSheetSize: this._maxSheetSize,
|
|
307
|
-
sheetChunkSize: this._sheetChunkSize,
|
|
308
|
-
onProgress: (loaded, total) => {
|
|
309
|
-
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
310
|
-
this._updateStatus(`Parsing ${file.name}... ${pct}%`, 'loading');
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
const sheetNames = Object.keys(sheets);
|
|
314
|
-
if (sheetNames.length === 0) {
|
|
315
|
-
this._updateStatus('No data found in file', 'error');
|
|
316
|
-
this.state.loading = false;
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (this._persistToIndexedDB) {
|
|
320
|
-
await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
|
|
321
|
-
}
|
|
322
|
-
if (sheetNames.length > 1) {
|
|
323
|
-
this._renderMultiSheet(sheets, file.name);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
this._setDataFrame(sheets[sheetNames[0]], file.name);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
const text = await file.text();
|
|
331
|
-
this._rawFileData = { file, text, isExcel: false };
|
|
332
|
-
const df = this._driver.parseCSV(text, {
|
|
333
|
-
autoDetectDelimiter: true,
|
|
334
|
-
hasHeader: true
|
|
335
|
-
});
|
|
336
|
-
if (this._persistToIndexedDB) {
|
|
337
|
-
await this._driver.store(file.name, df, { source: file.name });
|
|
338
|
-
}
|
|
339
|
-
this._setDataFrame(df, file.name);
|
|
340
|
-
}
|
|
222
|
+
this.state.loading = false;
|
|
223
|
+
this.state.loaded = true;
|
|
224
|
+
this.state.sourceName = result.sourceName;
|
|
225
|
+
this.state.rowCount = result.df?.height ?? 0;
|
|
226
|
+
this.state.colCount = result.df?.width ?? 0;
|
|
227
|
+
if (result.isMultiSheet && result.sheets) {
|
|
228
|
+
this._renderMultiSheet(result.sheets, result.sourceName);
|
|
341
229
|
}
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
this.state.loading = false;
|
|
345
|
-
this._updateStatus('Error: ' + err.message, 'error');
|
|
230
|
+
else if (result.df) {
|
|
231
|
+
this._renderSingleSheet(result.df);
|
|
346
232
|
}
|
|
233
|
+
this._updateStatus('', 'success');
|
|
234
|
+
this._triggerCallback('load', result.df, null, this);
|
|
347
235
|
}
|
|
348
|
-
|
|
349
|
-
* MULTI-SHEET RENDERING
|
|
350
|
-
* ═══════════════════════════════════════════════════ */
|
|
351
|
-
_renderMultiSheet(sheets, sourceName) {
|
|
236
|
+
_handleError(message) {
|
|
352
237
|
this.state.loading = false;
|
|
353
|
-
this.
|
|
238
|
+
this._updateStatus(message, 'error');
|
|
239
|
+
this._triggerCallback('error', message, null, this);
|
|
240
|
+
}
|
|
241
|
+
_handleProgress(loaded, total) {
|
|
242
|
+
const pct = total ? Math.round((loaded / total) * 100) : 0;
|
|
243
|
+
this._updateStatus(`Loading... ${pct}%`, 'loading');
|
|
244
|
+
}
|
|
245
|
+
_renderSingleSheet(df) {
|
|
246
|
+
if (this._table) {
|
|
247
|
+
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
248
|
+
this._table.columns(columnDefs).rows(df.toRows());
|
|
249
|
+
}
|
|
250
|
+
this._showDataView();
|
|
251
|
+
}
|
|
252
|
+
_renderMultiSheet(sheets, sourceName) {
|
|
354
253
|
const wrapper = document.getElementById(this._id);
|
|
355
254
|
if (!wrapper)
|
|
356
255
|
return;
|
|
357
|
-
|
|
358
|
-
if (existingTable)
|
|
359
|
-
existingTable.remove();
|
|
256
|
+
// Clear existing tabs
|
|
360
257
|
const existingTabs = wrapper.querySelector('.jux-tabs');
|
|
361
258
|
if (existingTabs)
|
|
362
259
|
existingTabs.remove();
|
|
363
|
-
Object.entries(sheets).forEach(([name, df]) => {
|
|
364
|
-
this._sheets.set(name, df);
|
|
365
|
-
});
|
|
366
260
|
const sheetNames = Object.keys(sheets);
|
|
367
261
|
const sanitizeId = (name) => name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
368
262
|
const tabDefs = sheetNames.map(name => ({
|
|
@@ -374,36 +268,18 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
374
268
|
tabs: tabDefs,
|
|
375
269
|
activeTab: sanitizeId(sheetNames[0])
|
|
376
270
|
});
|
|
377
|
-
const idToSheetName = new Map();
|
|
378
|
-
sheetNames.forEach(name => idToSheetName.set(sanitizeId(name), name));
|
|
379
|
-
this._tabs.bind('tabChange', (tabId) => {
|
|
380
|
-
const originalName = idToSheetName.get(tabId) || tabId;
|
|
381
|
-
this._df = this._sheets.get(originalName) || null;
|
|
382
|
-
});
|
|
383
271
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
384
272
|
if (dataContainer) {
|
|
385
273
|
dataContainer.style.display = '';
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
tabsContainer.className = 'jux-dataframe-tabs';
|
|
389
|
-
if (dataContainer) {
|
|
274
|
+
const tabsContainer = document.createElement('div');
|
|
275
|
+
tabsContainer.className = 'jux-dataframe-tabs';
|
|
390
276
|
dataContainer.appendChild(tabsContainer);
|
|
277
|
+
this._tabs.render(tabsContainer);
|
|
391
278
|
}
|
|
392
|
-
|
|
393
|
-
wrapper.appendChild(tabsContainer);
|
|
394
|
-
}
|
|
395
|
-
this._tabs.render(tabsContainer);
|
|
396
|
-
sheetNames.forEach((sheetName) => {
|
|
279
|
+
sheetNames.forEach(sheetName => {
|
|
397
280
|
const df = sheets[sheetName];
|
|
398
281
|
const safeId = sanitizeId(sheetName);
|
|
399
|
-
const table = new Table(`${this._id}-table-${safeId}`,
|
|
400
|
-
striped: this._tableOptions.striped,
|
|
401
|
-
hoverable: this._tableOptions.hoverable,
|
|
402
|
-
sortable: this._tableOptions.sortable,
|
|
403
|
-
filterable: this._tableOptions.filterable,
|
|
404
|
-
paginated: this._tableOptions.paginated,
|
|
405
|
-
rowsPerPage: this._tableOptions.rowsPerPage
|
|
406
|
-
});
|
|
282
|
+
const table = new Table(`${this._id}-table-${safeId}`, this._tableOptions);
|
|
407
283
|
const columnDefs = df.columns.map(col => ({ key: col, label: col }));
|
|
408
284
|
table.columns(columnDefs).rows(df.toRows());
|
|
409
285
|
const settingsBtn = new Button(`${this._id}-settings-${safeId}`, {
|
|
@@ -411,86 +287,37 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
411
287
|
variant: 'ghost',
|
|
412
288
|
size: 'small'
|
|
413
289
|
});
|
|
414
|
-
settingsBtn.bind('click', () => this.
|
|
290
|
+
settingsBtn.bind('click', () => this._showImportSettings());
|
|
415
291
|
this._tabs.addTabContent(safeId, [settingsBtn, table]);
|
|
416
292
|
});
|
|
417
|
-
const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
|
|
418
|
-
this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
|
|
419
293
|
this._showDataView();
|
|
420
|
-
this._df = sheets[sheetNames[0]];
|
|
421
|
-
this._triggerCallback('load', this._df, null, this);
|
|
422
294
|
}
|
|
423
295
|
/* ═══════════════════════════════════════════════════
|
|
424
|
-
*
|
|
296
|
+
* UI HELPERS
|
|
425
297
|
* ═══════════════════════════════════════════════════ */
|
|
426
|
-
_updateStatus(text, type
|
|
298
|
+
_updateStatus(text, type) {
|
|
427
299
|
const el = document.getElementById(`${this._id}-status`);
|
|
428
300
|
if (!el)
|
|
429
301
|
return;
|
|
430
302
|
if (type === 'loading' || type === 'error') {
|
|
431
303
|
el.style.display = '';
|
|
432
|
-
el.className =
|
|
433
|
-
el.
|
|
434
|
-
el.innerHTML = '';
|
|
435
|
-
if (this._icon && type === 'error') {
|
|
436
|
-
const iconEl = renderIcon(this._icon);
|
|
437
|
-
iconEl.style.width = '16px';
|
|
438
|
-
iconEl.style.height = '16px';
|
|
439
|
-
iconEl.style.marginRight = '6px';
|
|
440
|
-
iconEl.style.verticalAlign = 'middle';
|
|
441
|
-
el.appendChild(iconEl);
|
|
442
|
-
}
|
|
443
|
-
const span = document.createElement('span');
|
|
444
|
-
span.textContent = text;
|
|
445
|
-
el.appendChild(span);
|
|
304
|
+
el.className = `jux-dataframe-status jux-dataframe-status-${type}`;
|
|
305
|
+
el.textContent = text;
|
|
446
306
|
}
|
|
447
307
|
else {
|
|
448
308
|
el.style.display = 'none';
|
|
449
309
|
}
|
|
450
310
|
}
|
|
451
|
-
_setDataFrame(df, sourceName) {
|
|
452
|
-
this._df = df;
|
|
453
|
-
this.state.loaded = true;
|
|
454
|
-
this.state.loading = false;
|
|
455
|
-
this.state.sourceName = sourceName;
|
|
456
|
-
this.state.rowCount = df.height;
|
|
457
|
-
this.state.colCount = df.width;
|
|
458
|
-
const cols = df.columns;
|
|
459
|
-
const rows = df.toRows();
|
|
460
|
-
const emptyColumns = cols.filter(c => {
|
|
461
|
-
if (!c.startsWith('__EMPTY'))
|
|
462
|
-
return false;
|
|
463
|
-
return rows.every(row => {
|
|
464
|
-
const val = row[c];
|
|
465
|
-
return val === null || val === undefined || String(val).trim() === '';
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
if (emptyColumns.length > 0) {
|
|
469
|
-
const keepCols = cols.filter(c => !emptyColumns.includes(c));
|
|
470
|
-
if (keepCols.length > 0) {
|
|
471
|
-
this._df = df.select(...keepCols);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
if (this._table && this._df) {
|
|
475
|
-
const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
|
|
476
|
-
this._table.columns(columnDefs).rows(this._df.toRows());
|
|
477
|
-
}
|
|
478
|
-
this._showDataView();
|
|
479
|
-
this._updateStatus('', 'success');
|
|
480
|
-
this._triggerCallback('load', this._df, null, this);
|
|
481
|
-
}
|
|
482
311
|
_showDataView() {
|
|
483
312
|
const wrapper = document.getElementById(this._id);
|
|
484
313
|
if (!wrapper)
|
|
485
314
|
return;
|
|
486
315
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
|
|
487
|
-
if (uploadArea)
|
|
316
|
+
if (uploadArea)
|
|
488
317
|
uploadArea.style.display = 'none';
|
|
489
|
-
}
|
|
490
318
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
491
|
-
if (dataContainer)
|
|
319
|
+
if (dataContainer)
|
|
492
320
|
dataContainer.style.display = '';
|
|
493
|
-
}
|
|
494
321
|
this._updateSettingsGear();
|
|
495
322
|
}
|
|
496
323
|
_hideDataView() {
|
|
@@ -498,468 +325,61 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
498
325
|
if (!wrapper)
|
|
499
326
|
return;
|
|
500
327
|
const uploadArea = wrapper.querySelector('.jux-dataframe-upload-area');
|
|
501
|
-
if (uploadArea)
|
|
328
|
+
if (uploadArea)
|
|
502
329
|
uploadArea.style.display = '';
|
|
503
|
-
}
|
|
504
330
|
const dataContainer = wrapper.querySelector('.jux-dataframe-data');
|
|
505
|
-
if (dataContainer)
|
|
331
|
+
if (dataContainer)
|
|
506
332
|
dataContainer.style.display = 'none';
|
|
507
|
-
}
|
|
508
333
|
}
|
|
509
334
|
_updateSettingsGear() {
|
|
510
335
|
const gear = document.getElementById(`${this._id}-settings-gear`);
|
|
511
|
-
if (!gear || !this.
|
|
336
|
+
if (!gear || !this._source.df)
|
|
512
337
|
return;
|
|
513
|
-
const isMalformed = this.
|
|
338
|
+
const isMalformed = this._source.detectMalformed();
|
|
514
339
|
if (isMalformed) {
|
|
515
340
|
gear.classList.add('jux-dataframe-gear-warning');
|
|
516
341
|
gear.title = `${this.state.sourceName} — May need reformatting`;
|
|
517
342
|
}
|
|
518
343
|
else {
|
|
519
344
|
gear.classList.remove('jux-dataframe-gear-warning');
|
|
520
|
-
gear.title = `${this.state.sourceName} — ${this.
|
|
345
|
+
gear.title = `${this.state.sourceName} — ${this._source.height} rows × ${this._source.width} cols`;
|
|
521
346
|
}
|
|
522
347
|
}
|
|
523
348
|
_showSettingsModal() {
|
|
524
|
-
this.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const fileName = fileInfo?.name || this.state.sourceName || 'Unknown';
|
|
535
|
-
let contentHTML = `
|
|
536
|
-
<div class="jux-dataframe-settings-content">
|
|
537
|
-
<div class="jux-dataframe-settings-section">
|
|
538
|
-
<div class="jux-dataframe-settings-label">Source</div>
|
|
539
|
-
<div class="jux-dataframe-settings-value">
|
|
540
|
-
<strong>${this._escapeHtml(fileName)}</strong>
|
|
541
|
-
${fileInfo ? `<span class="jux-muted" style="margin-left: 8px;">${fileSizeKB} KB</span>` : ''}
|
|
542
|
-
</div>
|
|
543
|
-
</div>
|
|
544
|
-
<div class="jux-dataframe-settings-section">
|
|
545
|
-
<div class="jux-dataframe-settings-label">Data</div>
|
|
546
|
-
<div class="jux-dataframe-settings-value">
|
|
547
|
-
${this._df ? `${this._df.height} rows × ${this._df.width} columns` : 'No data loaded'}
|
|
548
|
-
${isMalformed ? '<span style="color: hsl(var(--warning)); margin-left: 8px;">⚠️ May need reformatting</span>' : ''}
|
|
549
|
-
</div>
|
|
550
|
-
</div>
|
|
551
|
-
`;
|
|
552
|
-
if (this._rawFileData) {
|
|
553
|
-
contentHTML += `
|
|
554
|
-
<div class="jux-dataframe-settings-section">
|
|
555
|
-
<div class="jux-dataframe-settings-label">Import</div>
|
|
556
|
-
<div class="jux-dataframe-settings-value">
|
|
557
|
-
<button id="${this._id}-adjust-import" class="jux-button jux-button-outline jux-button-sm">
|
|
558
|
-
⚙️ Adjust Header Row / Delimiter
|
|
559
|
-
</button>
|
|
560
|
-
</div>
|
|
561
|
-
</div>
|
|
562
|
-
`;
|
|
563
|
-
}
|
|
564
|
-
contentHTML += `</div>`;
|
|
565
|
-
this._settingsModal
|
|
566
|
-
.content(contentHTML)
|
|
567
|
-
.actions([
|
|
568
|
-
{
|
|
569
|
-
label: 'Remove Data',
|
|
570
|
-
variant: 'secondary',
|
|
571
|
-
click: async () => {
|
|
572
|
-
await this.clear();
|
|
573
|
-
this._settingsModal.closeModal();
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
label: 'Done',
|
|
578
|
-
variant: 'primary',
|
|
579
|
-
click: () => this._settingsModal.closeModal()
|
|
580
|
-
}
|
|
581
|
-
]);
|
|
582
|
-
this._settingsModal.render(document.body);
|
|
583
|
-
this._settingsModal.open();
|
|
584
|
-
requestAnimationFrame(() => {
|
|
585
|
-
const adjustBtn = document.getElementById(`${this._id}-adjust-import`);
|
|
586
|
-
if (adjustBtn) {
|
|
587
|
-
adjustBtn.addEventListener('click', () => {
|
|
588
|
-
this._settingsModal.closeModal();
|
|
589
|
-
this._showReshapeModal();
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
/* ═══════════════════════════════════════════════════
|
|
595
|
-
* MALFORMED DATA DETECTION
|
|
596
|
-
* ═══════════════════════════════════════════════════ */
|
|
597
|
-
_detectMalformedData(df) {
|
|
598
|
-
const columns = df.columns;
|
|
599
|
-
const rows = df.toRows();
|
|
600
|
-
const hasGenericColumns = columns.some(col => col.startsWith('__EMPTY') ||
|
|
601
|
-
col.match(/^_\d+$/) ||
|
|
602
|
-
col.match(/^col_\d+$/));
|
|
603
|
-
if (hasGenericColumns)
|
|
604
|
-
return true;
|
|
605
|
-
if (rows.length > 0) {
|
|
606
|
-
const firstRow = rows[0];
|
|
607
|
-
const values = Object.values(firstRow);
|
|
608
|
-
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
609
|
-
if (nonEmpty.length < columns.length * 0.5)
|
|
610
|
-
return true;
|
|
611
|
-
const hasMetadata = values.some(v => String(v).includes('Exported') ||
|
|
612
|
-
String(v).includes('Generated') ||
|
|
613
|
-
String(v).includes('Report'));
|
|
614
|
-
if (hasMetadata)
|
|
615
|
-
return true;
|
|
616
|
-
}
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
/* ═══════════════════════════════════════════════════
|
|
620
|
-
* RESHAPE MODAL
|
|
621
|
-
* ═══════════════════════════════════════════════════ */
|
|
622
|
-
_showReshapeModal() {
|
|
623
|
-
if (!this._rawFileData)
|
|
624
|
-
return;
|
|
625
|
-
if (this._rawFileData.isExcel) {
|
|
626
|
-
this._showExcelReshapeModal();
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
this._showCSVReshapeModal();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
_cleanupReshapeModal() {
|
|
633
|
-
if (this._reshapeModal && this._reshapeModalRendered) {
|
|
634
|
-
const oldEl = document.getElementById(`${this._id}-reshape-modal`);
|
|
635
|
-
if (oldEl)
|
|
636
|
-
oldEl.remove();
|
|
637
|
-
this._reshapeModal = null;
|
|
638
|
-
this._reshapeModalRendered = false;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
_escapeHtml(text) {
|
|
642
|
-
const div = document.createElement('div');
|
|
643
|
-
div.textContent = text;
|
|
644
|
-
return div.innerHTML;
|
|
645
|
-
}
|
|
646
|
-
_buildClickablePreviewHTML(rawRows, selectedSheetRow) {
|
|
647
|
-
let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
|
|
648
|
-
for (const { sheetRow, values } of rawRows) {
|
|
649
|
-
const isHeader = (sheetRow === selectedSheetRow);
|
|
650
|
-
const isSkipped = (sheetRow < selectedSheetRow);
|
|
651
|
-
let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
|
|
652
|
-
if (isHeader) {
|
|
653
|
-
rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
|
|
654
|
-
}
|
|
655
|
-
else if (isSkipped) {
|
|
656
|
-
rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
|
|
657
|
-
}
|
|
658
|
-
html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
|
|
659
|
-
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;">`;
|
|
660
|
-
if (isHeader) {
|
|
661
|
-
html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
html += `${sheetRow}`;
|
|
665
|
-
}
|
|
666
|
-
html += '</td>';
|
|
667
|
-
const displayCols = values.slice(0, 6);
|
|
668
|
-
displayCols.forEach(val => {
|
|
669
|
-
const displayVal = val != null ? String(val).substring(0, 20) : '';
|
|
670
|
-
const cellStyle = isHeader
|
|
671
|
-
? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
|
|
672
|
-
: 'padding: 8px 12px;';
|
|
673
|
-
html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
|
|
674
|
-
});
|
|
675
|
-
if (values.length > 6) {
|
|
676
|
-
html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
|
|
677
|
-
}
|
|
678
|
-
html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
|
|
679
|
-
if (isHeader) {
|
|
680
|
-
html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
|
|
681
|
-
}
|
|
682
|
-
else if (isSkipped) {
|
|
683
|
-
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
|
|
687
|
-
}
|
|
688
|
-
html += '</td></tr>';
|
|
689
|
-
}
|
|
690
|
-
html += '</table>';
|
|
691
|
-
return html;
|
|
692
|
-
}
|
|
693
|
-
async _showExcelReshapeModal() {
|
|
694
|
-
if (!this._rawFileData?.file)
|
|
695
|
-
return;
|
|
696
|
-
this._cleanupReshapeModal();
|
|
697
|
-
const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
|
|
698
|
-
if (rawRows.length === 0)
|
|
699
|
-
return;
|
|
700
|
-
let selectedSheetRow = rawRows[0].sheetRow;
|
|
701
|
-
for (const { sheetRow, values } of rawRows) {
|
|
702
|
-
const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
|
|
703
|
-
if (nonEmpty.length < values.length * 0.5)
|
|
704
|
-
continue;
|
|
705
|
-
const nonNumeric = nonEmpty.filter(v => {
|
|
706
|
-
const str = String(v).trim();
|
|
707
|
-
return isNaN(Number(str)) && str !== '';
|
|
708
|
-
}).length;
|
|
709
|
-
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
710
|
-
selectedSheetRow = sheetRow;
|
|
711
|
-
break;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
715
|
-
title: 'Excel Import Settings',
|
|
716
|
-
size: 'large',
|
|
717
|
-
close: true,
|
|
718
|
-
backdropClose: false
|
|
349
|
+
this._importModal.showSettings({
|
|
350
|
+
fileName: this._source.rawFileData?.file?.name || this.state.sourceName,
|
|
351
|
+
fileSize: this._source.rawFileData?.file?.size,
|
|
352
|
+
rowCount: this._source.height,
|
|
353
|
+
colCount: this._source.width,
|
|
354
|
+
isMalformed: this._source.detectMalformed(),
|
|
355
|
+
hasRawFileData: !!this._source.rawFileData
|
|
356
|
+
}, {
|
|
357
|
+
onClear: async () => { await this.clear(); },
|
|
358
|
+
onAdjustImport: () => { this._showImportSettings(); }
|
|
719
359
|
});
|
|
720
|
-
const modalContentHTML = `
|
|
721
|
-
<div style="margin-bottom: 1rem;">
|
|
722
|
-
<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>
|
|
723
|
-
<input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
|
|
724
|
-
</div>
|
|
725
|
-
<div>
|
|
726
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
727
|
-
<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>
|
|
728
|
-
</div>
|
|
729
|
-
`;
|
|
730
|
-
this._reshapeModal
|
|
731
|
-
.content(modalContentHTML)
|
|
732
|
-
.actions([
|
|
733
|
-
{
|
|
734
|
-
label: 'Cancel',
|
|
735
|
-
variant: 'secondary',
|
|
736
|
-
click: () => this._reshapeModal.closeModal()
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
label: 'Apply & Re-import',
|
|
740
|
-
variant: 'primary',
|
|
741
|
-
click: async () => {
|
|
742
|
-
const input = document.getElementById(`${this._id}-header-row`);
|
|
743
|
-
const headerRow = parseInt(input.value) || 0;
|
|
744
|
-
this.state.loading = true;
|
|
745
|
-
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
746
|
-
try {
|
|
747
|
-
const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
|
|
748
|
-
headerRow,
|
|
749
|
-
maxSheetSize: this._maxSheetSize,
|
|
750
|
-
sheetChunkSize: this._sheetChunkSize
|
|
751
|
-
});
|
|
752
|
-
const sheetNames = Object.keys(sheets);
|
|
753
|
-
if (sheetNames.length === 0) {
|
|
754
|
-
this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
|
|
755
|
-
this.state.loading = false;
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
|
|
759
|
-
if (sheetNames.length > 1) {
|
|
760
|
-
this._renderMultiSheet(sheets, this._rawFileData.file.name);
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
|
|
764
|
-
}
|
|
765
|
-
this._reshapeModal.closeModal();
|
|
766
|
-
}
|
|
767
|
-
catch (err) {
|
|
768
|
-
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
769
|
-
this.state.loading = false;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
]);
|
|
774
|
-
this._reshapeModal.render(document.body);
|
|
775
|
-
this._reshapeModalRendered = true;
|
|
776
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
777
|
-
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
778
|
-
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
779
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
780
|
-
const updateHint = (row) => {
|
|
781
|
-
if (!hintDiv)
|
|
782
|
-
return;
|
|
783
|
-
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
784
|
-
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
785
|
-
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
786
|
-
if (row > rawRows[0].sheetRow) {
|
|
787
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
788
|
-
}
|
|
789
|
-
else {
|
|
790
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
const renderPreview = (selected) => {
|
|
794
|
-
if (!previewDiv)
|
|
795
|
-
return;
|
|
796
|
-
previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
|
|
797
|
-
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
798
|
-
tr.addEventListener('click', () => {
|
|
799
|
-
const rowIdx = parseInt(tr.dataset.sheetRow);
|
|
800
|
-
hiddenInput.value = String(rowIdx);
|
|
801
|
-
updateHint(rowIdx);
|
|
802
|
-
renderPreview(rowIdx);
|
|
803
|
-
});
|
|
804
|
-
});
|
|
805
|
-
};
|
|
806
|
-
updateHint(selectedSheetRow);
|
|
807
|
-
renderPreview(selectedSheetRow);
|
|
808
|
-
this._reshapeModal.open();
|
|
809
360
|
}
|
|
810
|
-
|
|
811
|
-
|
|
361
|
+
_showImportSettings() {
|
|
362
|
+
const rawData = this._source.rawFileData;
|
|
363
|
+
if (!rawData)
|
|
812
364
|
return;
|
|
813
|
-
this.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const rawRows = [];
|
|
818
|
-
const maxPreviewRows = Math.min(lines.length, 15);
|
|
819
|
-
for (let i = 0; i < maxPreviewRows; i++) {
|
|
820
|
-
if (!lines[i]) {
|
|
821
|
-
rawRows.push({ sheetRow: i, values: [''] });
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
const values = this._driver._parseLine(lines[i], detected);
|
|
825
|
-
rawRows.push({ sheetRow: i, values });
|
|
826
|
-
}
|
|
827
|
-
let selectedRow = 0;
|
|
828
|
-
for (const { sheetRow, values } of rawRows) {
|
|
829
|
-
const nonEmpty = values.filter((v) => v.trim() !== '');
|
|
830
|
-
if (nonEmpty.length < values.length * 0.5)
|
|
831
|
-
continue;
|
|
832
|
-
const nonNumeric = nonEmpty.filter((v) => {
|
|
833
|
-
const trimmed = v.trim();
|
|
834
|
-
return isNaN(Number(trimmed)) && trimmed !== '';
|
|
835
|
-
}).length;
|
|
836
|
-
if (nonNumeric >= nonEmpty.length * 0.7) {
|
|
837
|
-
selectedRow = sheetRow;
|
|
838
|
-
break;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
|
|
842
|
-
title: 'CSV Import Settings',
|
|
843
|
-
size: 'large',
|
|
844
|
-
close: true,
|
|
845
|
-
backdropClose: false
|
|
846
|
-
});
|
|
847
|
-
const modalContentHTML = `
|
|
848
|
-
<div style="margin-bottom: 1rem;">
|
|
849
|
-
<label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
|
|
850
|
-
<select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
|
|
851
|
-
<option value=",">Comma (,)</option>
|
|
852
|
-
<option value="|">Pipe (|)</option>
|
|
853
|
-
<option value="	">Tab (\\t)</option>
|
|
854
|
-
<option value=";">Semicolon (;)</option>
|
|
855
|
-
</select>
|
|
856
|
-
</div>
|
|
857
|
-
<div style="margin-bottom: 1rem;">
|
|
858
|
-
<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>
|
|
859
|
-
<input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
|
|
860
|
-
</div>
|
|
861
|
-
<div>
|
|
862
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
|
|
863
|
-
<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>
|
|
864
|
-
</div>
|
|
865
|
-
`;
|
|
866
|
-
this._reshapeModal
|
|
867
|
-
.content(modalContentHTML)
|
|
868
|
-
.actions([
|
|
869
|
-
{
|
|
870
|
-
label: 'Cancel',
|
|
871
|
-
variant: 'secondary',
|
|
872
|
-
click: () => this._reshapeModal.closeModal()
|
|
873
|
-
},
|
|
874
|
-
{
|
|
875
|
-
label: 'Apply & Re-import',
|
|
876
|
-
variant: 'primary',
|
|
877
|
-
click: async () => {
|
|
878
|
-
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
879
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
880
|
-
const delim = delimiterSelect.value;
|
|
881
|
-
const headerRow = parseInt(hiddenInput.value) || 0;
|
|
882
|
-
this.state.loading = true;
|
|
883
|
-
this._updateStatus('Re-parsing with new settings...', 'loading');
|
|
884
|
-
try {
|
|
885
|
-
const df = this._driver.parseCSV(this._rawFileData.text, {
|
|
886
|
-
delimiter: delim,
|
|
887
|
-
headerRow,
|
|
888
|
-
hasHeader: true
|
|
889
|
-
});
|
|
890
|
-
await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
|
|
891
|
-
this._setDataFrame(df, this._rawFileData.file.name);
|
|
892
|
-
this._reshapeModal.closeModal();
|
|
893
|
-
}
|
|
894
|
-
catch (err) {
|
|
895
|
-
this._updateStatus(`Error: ${err.message}`, 'error');
|
|
896
|
-
this.state.loading = false;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
]);
|
|
901
|
-
this._reshapeModal.render(document.body);
|
|
902
|
-
this._reshapeModalRendered = true;
|
|
903
|
-
requestAnimationFrame(() => {
|
|
904
|
-
const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
|
|
905
|
-
const previewDiv = document.getElementById(`${this._id}-preview`);
|
|
906
|
-
const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
|
|
907
|
-
const hiddenInput = document.getElementById(`${this._id}-header-row`);
|
|
908
|
-
if (delimiterSelect)
|
|
909
|
-
delimiterSelect.value = detected;
|
|
910
|
-
const updateHint = (row) => {
|
|
911
|
-
if (!hintDiv)
|
|
912
|
-
return;
|
|
913
|
-
const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
|
|
914
|
-
const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
|
|
915
|
-
const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
|
|
916
|
-
if (row > rawRows[0].sheetRow) {
|
|
917
|
-
hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
|
|
365
|
+
this._importModal.showImportSettings(rawData, {
|
|
366
|
+
onReimport: async (result, sourceName) => {
|
|
367
|
+
if (result instanceof DataFrame) {
|
|
368
|
+
this._source.fromData(result.toRows(), sourceName);
|
|
918
369
|
}
|
|
919
370
|
else {
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
const reparse = () => {
|
|
924
|
-
const delim = delimiterSelect?.value || ',';
|
|
925
|
-
rawRows.length = 0;
|
|
926
|
-
for (let i = 0; i < maxPreviewRows; i++) {
|
|
927
|
-
if (!lines[i]) {
|
|
928
|
-
rawRows.push({ sheetRow: i, values: [''] });
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
const values = this._driver._parseLine(lines[i], delim);
|
|
932
|
-
rawRows.push({ sheetRow: i, values });
|
|
371
|
+
// Multi-sheet - handled by source callbacks
|
|
933
372
|
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
|
|
940
|
-
tr.addEventListener('click', () => {
|
|
941
|
-
const rowIdx = parseInt(tr.dataset.sheetRow);
|
|
942
|
-
hiddenInput.value = String(rowIdx);
|
|
943
|
-
updateHint(rowIdx);
|
|
944
|
-
renderPreview(rowIdx);
|
|
945
|
-
});
|
|
946
|
-
});
|
|
947
|
-
};
|
|
948
|
-
if (delimiterSelect) {
|
|
949
|
-
delimiterSelect.addEventListener('change', () => {
|
|
950
|
-
reparse();
|
|
951
|
-
const current = parseInt(hiddenInput.value) || 0;
|
|
952
|
-
updateHint(current);
|
|
953
|
-
renderPreview(current);
|
|
954
|
-
});
|
|
373
|
+
},
|
|
374
|
+
onError: (message) => this._handleError(message),
|
|
375
|
+
onStatusUpdate: (text, type) => {
|
|
376
|
+
this.state.loading = type === 'loading';
|
|
377
|
+
this._updateStatus(text, type);
|
|
955
378
|
}
|
|
956
|
-
updateHint(selectedRow);
|
|
957
|
-
renderPreview(selectedRow);
|
|
958
|
-
this._reshapeModal.open();
|
|
959
379
|
});
|
|
960
380
|
}
|
|
961
381
|
/* ═══════════════════════════════════════════════════
|
|
962
|
-
*
|
|
382
|
+
* RENDER
|
|
963
383
|
* ═══════════════════════════════════════════════════ */
|
|
964
384
|
update(_prop, _value) { }
|
|
965
385
|
render(targetId) {
|
|
@@ -976,37 +396,33 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
976
396
|
if (this._inlineUpload) {
|
|
977
397
|
const uploadArea = document.createElement('div');
|
|
978
398
|
uploadArea.className = 'jux-dataframe-upload-area';
|
|
979
|
-
uploadArea.id = `${this._id}-upload-area`;
|
|
980
399
|
const uploadOpts = {
|
|
981
400
|
label: this._uploadButtonLabel,
|
|
982
401
|
accept: this._uploadAccept,
|
|
983
402
|
variant: this._uploadButtonVariant,
|
|
984
403
|
};
|
|
985
|
-
if (this._showUploadIcon
|
|
404
|
+
if (this._showUploadIcon)
|
|
986
405
|
uploadOpts.icon = this._uploadButtonIcon;
|
|
987
|
-
}
|
|
988
406
|
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
989
407
|
this._uploadRef = upload;
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
});
|
|
998
|
-
|
|
408
|
+
// Wire upload to source
|
|
409
|
+
upload.bind('change', async (files) => {
|
|
410
|
+
if (!files || files.length === 0) {
|
|
411
|
+
await this.clear();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
this.state.loading = true;
|
|
415
|
+
this._updateStatus(`Parsing ${files[0].name}...`, 'loading');
|
|
416
|
+
await this._source.fromFile(files[0]);
|
|
417
|
+
});
|
|
999
418
|
const uploadContainer = document.createElement('div');
|
|
1000
419
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
1001
|
-
uploadContainer.id = `${this._id}-upload-container`;
|
|
1002
420
|
uploadArea.appendChild(uploadContainer);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
uploadArea.appendChild(statusBar);
|
|
1009
|
-
}
|
|
421
|
+
const statusBar = document.createElement('div');
|
|
422
|
+
statusBar.className = 'jux-dataframe-status';
|
|
423
|
+
statusBar.id = `${this._id}-status`;
|
|
424
|
+
statusBar.style.display = 'none';
|
|
425
|
+
uploadArea.appendChild(statusBar);
|
|
1010
426
|
if (this._uploadDescription) {
|
|
1011
427
|
const descEl = document.createElement('div');
|
|
1012
428
|
descEl.className = 'jux-dataframe-upload-description';
|
|
@@ -1037,14 +453,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
1037
453
|
dataContainer.appendChild(toolbar);
|
|
1038
454
|
wrapper.appendChild(dataContainer);
|
|
1039
455
|
// Table
|
|
1040
|
-
const tbl = new Table(`${this._id}-table`,
|
|
1041
|
-
striped: this._tableOptions.striped,
|
|
1042
|
-
hoverable: this._tableOptions.hoverable,
|
|
1043
|
-
sortable: this._tableOptions.sortable,
|
|
1044
|
-
filterable: this._tableOptions.filterable,
|
|
1045
|
-
paginated: this._tableOptions.paginated,
|
|
1046
|
-
rowsPerPage: this._tableOptions.rowsPerPage
|
|
1047
|
-
});
|
|
456
|
+
const tbl = new Table(`${this._id}-table`, this._tableOptions);
|
|
1048
457
|
tbl.render(dataContainer);
|
|
1049
458
|
this._table = tbl;
|
|
1050
459
|
if (this._pendingSource) {
|