e-pick 1.0.0 → 3.0.0

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.
@@ -0,0 +1,668 @@
1
+ /**
2
+ * UI Manager
3
+ *
4
+ * Handles all UI updates and interactions
5
+ */
6
+
7
+ class UIManager {
8
+ constructor(apiFacade = null) {
9
+ this.api = apiFacade || new APIFacade();
10
+ this.elements = {};
11
+ this.currentStep = 1;
12
+ this.completedSteps = new Set();
13
+ }
14
+
15
+ /**
16
+ * Initialize UI elements
17
+ */
18
+ init() {
19
+ this.elements = {
20
+ fileInput: document.getElementById('file-input'),
21
+ sheetSelector: document.getElementById('sheet-selector'),
22
+ sheetSearch: document.getElementById('sheet-search'),
23
+ sheetCheckboxes: document.getElementById('sheet-checkboxes'),
24
+ loadSheetsBtn: document.getElementById('load-sheets-btn'),
25
+ dataPreview: document.getElementById('data-preview'),
26
+ columnSelectors: document.getElementById('column-selectors'),
27
+ commitColumnSelect: document.getElementById('commit-column'),
28
+ repoColumnSelect: document.getElementById('repo-column'),
29
+ validateBtn: document.getElementById('validate-btn'),
30
+ validationResults: document.getElementById('validation-results'),
31
+ validationDetails: document.getElementById('validation-details'),
32
+ validCommitsFilterStats: document.getElementById('valid-commits-filter-stats'),
33
+ validCommitsList: document.getElementById('valid-commits-list'),
34
+ invalidCommitsList: document.getElementById('invalid-commits-list'),
35
+ commandOutput: document.getElementById('command-output'),
36
+ commandText: document.getElementById('command-text'),
37
+ copyBtn: document.getElementById('copy-btn'),
38
+ viewFilesBtn: document.getElementById('view-files-btn'),
39
+ generateBtn: document.getElementById('generate-btn'),
40
+ filesChanged: document.getElementById('files-changed'),
41
+ filesList: document.getElementById('files-list'),
42
+ compareBranch: document.getElementById('compare-branch'),
43
+ loadingSpinner: document.getElementById('loading'),
44
+ repoInfo: document.getElementById('repo-info'),
45
+ stepper: document.getElementById('stepper')
46
+ };
47
+
48
+ this.changedFilesData = null;
49
+
50
+ this.setupStepperNavigation();
51
+ this.setupSheetSearch();
52
+ this.showStep(1);
53
+ }
54
+
55
+ /**
56
+ * Setup sheet search functionality
57
+ */
58
+ setupSheetSearch() {
59
+ if (this.elements.sheetSearch) {
60
+ this.elements.sheetSearch.addEventListener('input', (e) => {
61
+ this.filterSheets(e.target.value);
62
+ });
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Filter sheet checkboxes based on search term
68
+ * @param {string} searchTerm - Search term
69
+ */
70
+ filterSheets(searchTerm) {
71
+ const labels = this.elements.sheetCheckboxes.querySelectorAll('.sheet-checkbox-label');
72
+ const term = searchTerm.toLowerCase().trim();
73
+
74
+ labels.forEach(label => {
75
+ const sheetName = label.querySelector('span').textContent.toLowerCase();
76
+ if (sheetName.includes(term)) {
77
+ label.style.display = 'flex';
78
+ } else {
79
+ label.style.display = 'none';
80
+ }
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Setup stepper click handlers
86
+ */
87
+ setupStepperNavigation() {
88
+ const stepCircles = this.elements.stepper.querySelectorAll('.step-circle');
89
+ stepCircles.forEach(circle => {
90
+ circle.addEventListener('click', (e) => {
91
+ const step = parseInt(e.target.getAttribute('data-step'));
92
+ // Only allow navigation to completed steps
93
+ if (this.completedSteps.has(step) || step < this.currentStep) {
94
+ this.goToStep(step);
95
+ }
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Navigate to a specific step
102
+ * @param {number} step - Step number to navigate to
103
+ */
104
+ goToStep(step) {
105
+ if (step < 1 || step > 6) return;
106
+ this.currentStep = step;
107
+ this.showStep(step);
108
+ this.updateStepperUI();
109
+ }
110
+
111
+ /**
112
+ * Show only content for the current step
113
+ * @param {number} step - Step number to show
114
+ */
115
+ showStep(step) {
116
+ // Hide all step contents
117
+ const allStepContents = document.querySelectorAll('[data-step-content]');
118
+ allStepContents.forEach(section => {
119
+ section.style.display = 'none';
120
+ });
121
+
122
+ // Show only current step content
123
+ const currentStepContents = document.querySelectorAll(`[data-step-content="${step}"]`);
124
+ currentStepContents.forEach(section => {
125
+ // Only show if it's not the sheet selector or it has content
126
+ if (section.id !== 'sheet-selector' || section.querySelector('select')?.options.length > 0) {
127
+ section.style.display = 'block';
128
+ }
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Advance to the next step
134
+ * @param {number} nextStep - Next step number
135
+ */
136
+ advanceToStep(nextStep) {
137
+ // Mark current step as completed
138
+ this.completedSteps.add(this.currentStep);
139
+
140
+ // Move to next step
141
+ this.currentStep = nextStep;
142
+ this.showStep(nextStep);
143
+ this.updateStepperUI();
144
+
145
+ // Scroll stepper into view
146
+ this.elements.stepper.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
147
+ }
148
+
149
+ /**
150
+ * Update stepper UI to reflect current state
151
+ */
152
+ updateStepperUI() {
153
+ const stepperItems = this.elements.stepper.querySelectorAll('.stepper-item');
154
+
155
+ stepperItems.forEach((item, index) => {
156
+ const circle = item.querySelector('.step-circle');
157
+ const step = parseInt(circle.getAttribute('data-step'));
158
+
159
+ // Remove all classes
160
+ circle.classList.remove('active', 'completed');
161
+ item.classList.remove('active', 'completed');
162
+
163
+ // Add appropriate class to both item and circle
164
+ if (step === this.currentStep) {
165
+ circle.classList.add('active');
166
+ item.classList.add('active');
167
+ } else if (this.completedSteps.has(step) || step < this.currentStep) {
168
+ circle.classList.add('completed');
169
+ item.classList.add('completed');
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Show loading state
176
+ * @param {string} message - Loading message
177
+ */
178
+ showLoading(message = 'Processing...') {
179
+ this.elements.loadingSpinner.textContent = message;
180
+ this.elements.loadingSpinner.style.display = 'block';
181
+ }
182
+
183
+ /**
184
+ * Hide loading state
185
+ */
186
+ hideLoading() {
187
+ this.elements.loadingSpinner.style.display = 'none';
188
+ }
189
+
190
+ /**
191
+ * Show toast notification
192
+ * @param {string} message - Message to display
193
+ * @param {string} type - Type of toast (success, error, info)
194
+ */
195
+ showToast(message, type = 'info') {
196
+ const toast = document.createElement('div');
197
+ toast.className = `toast toast-${type}`;
198
+ toast.textContent = message;
199
+
200
+ document.body.appendChild(toast);
201
+
202
+ setTimeout(() => toast.classList.add('show'), 100);
203
+ setTimeout(() => {
204
+ toast.classList.remove('show');
205
+ setTimeout(() => toast.remove(), 300);
206
+ }, 3000);
207
+ }
208
+
209
+ /**
210
+ * Show sheet selector for Excel files
211
+ * @param {array} sheets - Array of sheet names
212
+ */
213
+ showSheetSelector(sheets) {
214
+ const checkboxes = sheets.map((sheet, index) => `
215
+ <label class="sheet-checkbox-label">
216
+ <input type="checkbox" name="sheet" value="${sheet}" ${index === 0 ? 'checked' : ''} onchange="window.app.ui.updateLoadSheetsButton()">
217
+ <span>${sheet}</span>
218
+ </label>
219
+ `).join('');
220
+
221
+ this.elements.sheetCheckboxes.innerHTML = checkboxes;
222
+ this.elements.sheetSelector.style.display = 'block';
223
+ this.elements.loadSheetsBtn.disabled = false;
224
+
225
+ // Clear search input
226
+ if (this.elements.sheetSearch) {
227
+ this.elements.sheetSearch.value = '';
228
+ }
229
+
230
+ // Auto-scroll to sheet selector
231
+ setTimeout(() => {
232
+ this.elements.sheetSelector.scrollIntoView({ behavior: 'smooth', block: 'start' });
233
+ }, 100);
234
+ }
235
+
236
+ /**
237
+ * Update load sheets button state
238
+ */
239
+ updateLoadSheetsButton() {
240
+ const checkedBoxes = this.elements.sheetCheckboxes.querySelectorAll('input[type="checkbox"]:checked');
241
+ this.elements.loadSheetsBtn.disabled = checkedBoxes.length === 0;
242
+ }
243
+
244
+ /**
245
+ * Get selected sheet names
246
+ * @returns {array} Array of selected sheet names
247
+ */
248
+ getSelectedSheets() {
249
+ const checkedBoxes = this.elements.sheetCheckboxes.querySelectorAll('input[type="checkbox"]:checked');
250
+ return Array.from(checkedBoxes).map(cb => cb.value);
251
+ }
252
+
253
+ /**
254
+ * Hide sheet selector
255
+ */
256
+ hideSheetSelector() {
257
+ this.elements.sheetSelector.style.display = 'none';
258
+ }
259
+
260
+ /**
261
+ * Render data preview table
262
+ * @param {object} parsedData - Parsed file data
263
+ */
264
+ renderDataPreview(parsedData) {
265
+ const { columns, rows, currentSheet } = parsedData;
266
+
267
+ // Show only first 10 rows
268
+ const previewRows = rows.slice(0, 10);
269
+
270
+ let html = `
271
+ <div class="preview-header">
272
+ <h3>Data Preview${currentSheet ? ` - Sheet: ${currentSheet}` : ''}</h3>
273
+ <p class="info-text">${rows.length} rows total, showing first 10</p>
274
+ </div>
275
+ <div class="table-container" tabindex="0" role="region" aria-label="Data preview table">
276
+ <table>
277
+ <thead>
278
+ <tr>
279
+ ${columns.map(col => `<th>${col.name}</th>`).join('')}
280
+ </tr>
281
+ </thead>
282
+ <tbody>
283
+ ${previewRows.map(row => `
284
+ <tr>
285
+ ${row.map(cell => `<td>${cell || ''}</td>`).join('')}
286
+ </tr>
287
+ `).join('')}
288
+ </tbody>
289
+ </table>
290
+ </div>
291
+ `;
292
+
293
+ this.elements.dataPreview.innerHTML = html;
294
+
295
+ // Populate column selectors
296
+ this.populateColumnSelectors(columns);
297
+
298
+ // Advance to step 2
299
+ this.advanceToStep(2);
300
+ }
301
+
302
+ /**
303
+ * Populate column selector dropdowns
304
+ * @param {array} columns - Column definitions
305
+ */
306
+ populateColumnSelectors(columns) {
307
+ const commitOptions = columns.map(col =>
308
+ `<option value="${col.index}">${col.name}</option>`
309
+ ).join('');
310
+
311
+ const repoOptions = columns.map(col =>
312
+ `<option value="${col.index}">${col.name}</option>`
313
+ ).join('');
314
+
315
+ this.elements.commitColumnSelect.innerHTML = commitOptions;
316
+ this.elements.repoColumnSelect.innerHTML = repoOptions;
317
+
318
+ // Try to auto-select commit column (look for "commit" in name)
319
+ const commitCol = columns.find(col =>
320
+ col && col.name && typeof col.name === 'string' &&
321
+ col.name.toLowerCase().includes('commit')
322
+ );
323
+ if (commitCol) {
324
+ this.elements.commitColumnSelect.value = commitCol.index;
325
+ }
326
+
327
+ // Try to auto-select repo column
328
+ const repoCol = columns.find(col =>
329
+ col && col.name && typeof col.name === 'string' &&
330
+ col.name.toLowerCase().includes('repo')
331
+ );
332
+ if (repoCol) {
333
+ this.elements.repoColumnSelect.value = repoCol.index;
334
+ }
335
+
336
+ this.elements.validateBtn.disabled = false;
337
+ }
338
+
339
+ /**
340
+ * Render validation results
341
+ * @param {object} results - Validation results
342
+ * @param {function} onToggleIgnore - Callback for ignore toggle
343
+ */
344
+ renderValidationResults(results, onToggleIgnore) {
345
+ const { results: commits, summary, filterStats } = results;
346
+
347
+ const validCommits = commits.filter(c => c.isValid);
348
+ const invalidCommits = commits.filter(c => !c.isValid);
349
+
350
+ // Add filter statistics if available
351
+ let filterStatsHtml = '';
352
+ if (filterStats) {
353
+ const matchMethod = filterStats.matchedByCommit
354
+ ? `<p style="margin-bottom: 0.25rem;"><strong>Match Method:</strong> <span class="success-text">Validated by commit existence</span></p>`
355
+ : '';
356
+
357
+ const repoInfo = filterStats.matchedRepo
358
+ ? `<p style="margin-bottom: 0.25rem;"><strong>Matched Repository:</strong> ${filterStats.matchedRepo}</p>`
359
+ : '';
360
+
361
+ filterStatsHtml = `
362
+ <div class="filter-stats ${filterStats.filtered > 0 ? 'warning-box' : 'success-box'}" style="margin-bottom: 1rem;">
363
+ <h3 style="margin-bottom: 0.5rem;">📊 Repository Filter Results</h3>
364
+ ${repoInfo}
365
+ ${matchMethod}
366
+ <p style="margin-bottom: 0.25rem;"><strong>Total Commits in File:</strong> ${filterStats.total}</p>
367
+ <p style="margin-bottom: 0.25rem;"><strong>Commits for This Repo:</strong> <span class="success-text">${filterStats.matched}</span></p>
368
+ ${filterStats.filtered > 0 ? `
369
+ <p style="margin-bottom: 0;" class="warning-text"><strong>Filtered Out (Other Repos):</strong> ${filterStats.filtered}</p>
370
+ <p class="muted-text" style="margin-top: 0.5rem;">
371
+ 💡 ${filterStats.filtered} commit(s) from other repositories were automatically excluded
372
+ </p>
373
+ ` : ''}
374
+ ${filterStats.availableRepos && filterStats.availableRepos.length > 1 ? `
375
+ <p class="muted-text" style="margin-top: 0.5rem;">
376
+ Repositories in file: ${filterStats.availableRepos.join(', ')}
377
+ </p>
378
+ ` : ''}
379
+ ${filterStats.warning ? `
380
+ <p class="warning-text muted-text" style="margin-top: 0.5rem;">
381
+ ⚠️ ${filterStats.warning}
382
+ </p>
383
+ ` : ''}
384
+ </div>
385
+ `;
386
+ }
387
+
388
+ // Render invalid commits with note (SHOW FIRST)
389
+ const invalidNote = filterStats && filterStats.filtered > 0
390
+ ? `<p class="muted-text" style="margin-bottom: 1rem;">
391
+ <strong>Note:</strong> Showing only invalid commits from the matched repository.
392
+ Commits from other repositories (${filterStats.filtered}) were filtered out automatically.
393
+ </p>`
394
+ : '';
395
+
396
+ this.elements.invalidCommitsList.innerHTML = invalidNote + (invalidCommits.length > 0
397
+ ? invalidCommits.map(c => this._renderInvalidCommit(c, onToggleIgnore)).join('')
398
+ : '<p class="empty">No invalid commits in this repository</p>');
399
+
400
+ // Render filter stats (outside scroll)
401
+ this.elements.validCommitsFilterStats.innerHTML = filterStatsHtml;
402
+
403
+ // Render valid commits (inside scroll)
404
+ this.elements.validCommitsList.innerHTML = (validCommits.length > 0
405
+ ? validCommits.map(c => this._renderValidCommit(c)).join('')
406
+ : '<p class="empty">No valid commits found</p>');
407
+
408
+ // Show validation details section
409
+ this.elements.validationDetails.style.display = 'block';
410
+
411
+ // Enable generate button if there are valid commits (but keep it hidden)
412
+ this.elements.generateBtn.disabled = validCommits.length === 0;
413
+
414
+ // Advance to step 4
415
+ this.advanceToStep(4);
416
+ }
417
+
418
+ /**
419
+ * Render a valid commit
420
+ * @param {object} commit - Commit data
421
+ * @returns {string} HTML string
422
+ * @private
423
+ */
424
+ _renderValidCommit(commit) {
425
+ const { commitInfo } = commit;
426
+ return `
427
+ <div class="commit-item commit-valid">
428
+ <div class="commit-hash">
429
+ <span class="badge badge-success">✓</span>
430
+ <code>${commitInfo.shortHash}</code>
431
+ </div>
432
+ <div class="commit-details">
433
+ <div class="commit-message">${commitInfo.message}</div>
434
+ <div class="commit-meta">
435
+ ${commitInfo.author} • ${new Date(commitInfo.date).toLocaleString()}
436
+ </div>
437
+ </div>
438
+ </div>
439
+ `;
440
+ }
441
+
442
+ /**
443
+ * Render an invalid commit
444
+ * @param {object} commit - Commit data
445
+ * @param {function} onToggleIgnore - Callback for ignore toggle
446
+ * @returns {string} HTML string
447
+ * @private
448
+ */
449
+ _renderInvalidCommit(commit, onToggleIgnore) {
450
+ const badgeClass = commit.canIgnore ? 'badge-warning' : 'badge-error';
451
+ const badgeIcon = commit.canIgnore ? '⚠' : '✗';
452
+
453
+ return `
454
+ <div class="commit-item commit-invalid" style="padding: 0.75rem;">
455
+ <details>
456
+ <summary style="cursor: pointer; display: flex; align-items: center; gap: 1rem; list-style: none;">
457
+ <span class="badge ${badgeClass}">${badgeIcon}</span>
458
+ <code class="strong-text">${commit.commit.substring(0, 7)}</code>
459
+ <span class="error-text" style="flex: 1;">${commit.error.message}</span>
460
+ <span class="muted-text">▼ View Details</span>
461
+ </summary>
462
+ <div class="error-box" style="margin-top: 1rem;">
463
+ <div class="commit-details">
464
+ <div style="margin-bottom: 0.5rem;">
465
+ <strong class="strong-text">Full Hash:</strong>
466
+ <code class="inline-code" style="display: block; margin-top: 0.25rem;">${commit.commit}</code>
467
+ </div>
468
+ <div style="margin-bottom: 0.5rem;">
469
+ <strong class="strong-text">Error Type:</strong>
470
+ <code class="commit-error-type" style="margin-left: 0.5rem;">${commit.error.type}</code>
471
+ </div>
472
+ <div style="margin-bottom: 0.5rem;">
473
+ <strong class="strong-text">Error Message:</strong>
474
+ <p class="error-text" style="margin-top: 0.25rem;">${commit.error.message}</p>
475
+ </div>
476
+ ${commit.canIgnore ? `
477
+ <div class="warning-box" style="margin-top: 1rem;">
478
+ <label class="ignore-checkbox">
479
+ <input type="checkbox" data-commit="${commit.commit}" onchange="window.app.handleIgnoreToggle('${commit.commit}')">
480
+ <span class="warning-text">Ignore this commit (can be safely skipped)</span>
481
+ </label>
482
+ </div>
483
+ ` : ''}
484
+ </div>
485
+ </div>
486
+ </details>
487
+ </div>
488
+ `;
489
+ }
490
+
491
+ /**
492
+ * Display generated command
493
+ * @param {object} commandData - Command data
494
+ */
495
+ displayCommand(commandData) {
496
+ const { command, commits, warning, changedFiles } = commandData;
497
+
498
+ const fileCount = changedFiles ? changedFiles.length : 0;
499
+
500
+ let html = `
501
+ <div class="command-box">
502
+ <pre><code>${command}</code></pre>
503
+ </div>
504
+ <div class="command-info" style="margin-bottom: 1.5rem; display: flex; gap: 2rem; align-items: center;">
505
+ <p><strong>Commits to cherry-pick:</strong> ${commits.length}</p>
506
+ <p class="success-box" style="padding: 0.5rem 1rem; margin: 0;">
507
+ <strong>Files changed:</strong> <span class="success-text" style="font-size: 1.1rem;">${fileCount}</span>
508
+ </p>
509
+ </div>
510
+ ${warning ? `<p class="warning" style="margin-bottom: 1rem;">⚠ ${warning}</p>` : ''}
511
+
512
+ <details open style="margin-top: 1rem;">
513
+ <summary class="details-summary">
514
+ 📋 Commit Order Details (${commits.length} commits)
515
+ </summary>
516
+ <div class="details-content" tabindex="0" role="region" aria-label="Commit order details">
517
+ ${commits.map((c, i) => `
518
+ <div class="commit-order-item">
519
+ <span class="order-num">${i + 1}</span>
520
+ <code>${c.shortHash}</code>
521
+ <span class="commit-msg">${c.message}</span>
522
+ </div>
523
+ `).join('')}
524
+ </div>
525
+ </details>
526
+ `;
527
+
528
+ this.elements.commandText.innerHTML = html;
529
+ this.elements.copyBtn.disabled = false;
530
+
531
+ // Store command and changed files for later use
532
+ this.currentCommand = command;
533
+ this.changedFilesData = changedFiles || [];
534
+
535
+ // Always enable view files button - displayChangedFiles() will handle empty state
536
+ this.elements.viewFilesBtn.disabled = false;
537
+
538
+ // Show command output section (stay in step 4)
539
+ this.elements.commandOutput.style.display = 'block';
540
+
541
+ // Scroll to command output smoothly
542
+ setTimeout(() => {
543
+ this.elements.commandOutput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
544
+ }, 100);
545
+ }
546
+
547
+ /**
548
+ * Display changed files with diff commands
549
+ */
550
+ displayChangedFiles() {
551
+ if (!this.changedFilesData || this.changedFilesData.length === 0) {
552
+ this.elements.filesList.innerHTML = '<p class="empty">No files changed</p>';
553
+ return;
554
+ }
555
+
556
+ const branch = this.elements.compareBranch.value || 'origin/develop';
557
+
558
+ // Create single command to check only the changed files from commits
559
+ const diffCommand = `git diff ${branch} -- ${this.changedFilesData.join(' ')}`;
560
+
561
+ // Create files list
562
+ const filesHtml = this.changedFilesData.map((file, index) => {
563
+ return `
564
+ <div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0;">
565
+ <span class="badge-number">${index + 1}</span>
566
+ <code class="file-code">${file}</code>
567
+ </div>
568
+ `;
569
+ }).join('');
570
+
571
+ this.elements.filesList.innerHTML = `
572
+ <!-- Command to check all files -->
573
+ <div class="success-box" style="margin-bottom: 1.5rem;">
574
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
575
+ <h3 class="success-text" style="margin: 0; font-size: 1.1rem;">📝 Git Diff Command</h3>
576
+ <button
577
+ class="btn btn-primary"
578
+ style="padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0;"
579
+ onclick="navigator.clipboard.writeText('${diffCommand}'); window.app.ui.showToast('Copied!', 'success');"
580
+ >
581
+ 📋 Copy Command
582
+ </button>
583
+ </div>
584
+ <p class="muted-text" style="margin-bottom: 1rem;">
585
+ Use this command to see all changes compared to <strong>${branch}</strong> branch:
586
+ </p>
587
+ <div class="code-box">
588
+ <pre style="margin: 0; overflow-x: auto;"><code class="code-text">${diffCommand}</code></pre>
589
+ </div>
590
+ </div>
591
+
592
+ <!-- Files list -->
593
+ <div class="info-box">
594
+ <h3 class="strong-text" style="margin: 0 0 1rem 0; font-size: 1rem;">
595
+ 📂 Changed Files (<strong>${this.changedFilesData.length}</strong> files)
596
+ </h3>
597
+ <div class="details-content" style="padding-right: 0.5rem;" tabindex="0" role="region" aria-label="Changed files list">
598
+ ${filesHtml}
599
+ </div>
600
+ </div>
601
+ `;
602
+
603
+ // Add event listener for branch input change (only once)
604
+ const branchInput = this.elements.compareBranch;
605
+ const newBranchInput = branchInput.cloneNode(true);
606
+ branchInput.parentNode.replaceChild(newBranchInput, branchInput);
607
+ this.elements.compareBranch = newBranchInput;
608
+
609
+ newBranchInput.addEventListener('input', () => {
610
+ this.displayChangedFiles();
611
+ });
612
+
613
+ // Advance to step 5 (View Changed Files)
614
+ this.advanceToStep(5);
615
+ }
616
+
617
+ /**
618
+ * Copy command to clipboard
619
+ */
620
+ async copyToClipboard() {
621
+ if (!this.currentCommand) return;
622
+
623
+ try {
624
+ await navigator.clipboard.writeText(this.currentCommand);
625
+ this.showToast('Command copied to clipboard!', 'success');
626
+ } catch (error) {
627
+ this.showToast('Failed to copy command', 'error');
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Load and display repository info
633
+ */
634
+ async loadRepoInfo() {
635
+ try {
636
+ const data = await this.api.getRepoInfo();
637
+
638
+ if (data.success) {
639
+ const { repo } = data;
640
+ this.elements.repoInfo.innerHTML = `
641
+ <strong>${repo.repoName}</strong> •
642
+ Branch: ${repo.branch} •
643
+ Last commit: ${repo.lastCommit.hash}
644
+ `;
645
+ }
646
+ } catch (error) {
647
+ console.error('Failed to load repo info:', error);
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Load and display version from API
653
+ */
654
+ async loadVersion() {
655
+ try {
656
+ const data = await this.api.getVersion();
657
+ const versionEl = document.getElementById('version-info');
658
+ if (versionEl && data.version) {
659
+ versionEl.textContent = `E-Pick Tool v${data.version}`;
660
+ }
661
+ } catch (error) {
662
+ console.error('Failed to load version:', error);
663
+ }
664
+ }
665
+ }
666
+
667
+ // Export for use in other scripts
668
+ window.UIManager = UIManager;