claude-code-workflow 6.3.29 → 6.3.31
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/ccw/dist/cli.js +1 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts +0 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +3 -3
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/core/data-aggregator.js +20 -7
- package/ccw/dist/core/data-aggregator.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/issue-routes.js +316 -4
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/dist/tools/cli-executor-core.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor-core.js +6 -32
- package/ccw/dist/tools/cli-executor-core.js.map +1 -1
- package/ccw/src/cli.ts +1 -1
- package/ccw/src/commands/cli.ts +4 -4
- package/ccw/src/core/data-aggregator.ts +19 -7
- package/ccw/src/core/routes/issue-routes.ts +356 -4
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +463 -37
- package/ccw/src/templates/dashboard-js/i18n.js +38 -0
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +5 -5
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +852 -29
- package/ccw/src/templates/dashboard-js/views/skills-manager.js +2 -4
- package/ccw/src/tools/cli-executor-core.ts +7 -33
- package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-312.pyc +0 -0
- package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/cli/commands.py +78 -0
- package/package.json +1 -1
|
@@ -13,7 +13,11 @@ var issueData = {
|
|
|
13
13
|
selectedSolutionIssueId: null,
|
|
14
14
|
statusFilter: 'all',
|
|
15
15
|
searchQuery: '',
|
|
16
|
-
viewMode: 'issues' // 'issues' | 'queue'
|
|
16
|
+
viewMode: 'issues', // 'issues' | 'queue'
|
|
17
|
+
// Search suggestions state
|
|
18
|
+
searchSuggestions: [],
|
|
19
|
+
showSuggestions: false,
|
|
20
|
+
selectedSuggestion: -1
|
|
17
21
|
};
|
|
18
22
|
var issueLoading = false;
|
|
19
23
|
var issueDragState = {
|
|
@@ -21,6 +25,13 @@ var issueDragState = {
|
|
|
21
25
|
groupId: null
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
// Multi-queue state
|
|
29
|
+
var queueData = {
|
|
30
|
+
queues: [], // All queue index entries
|
|
31
|
+
activeQueueId: null, // Currently active queue
|
|
32
|
+
expandedQueueId: null // Queue showing execution groups
|
|
33
|
+
};
|
|
34
|
+
|
|
24
35
|
// ========== Main Render Function ==========
|
|
25
36
|
async function renderIssueManager() {
|
|
26
37
|
const container = document.getElementById('mainContent');
|
|
@@ -36,7 +47,7 @@ async function renderIssueManager() {
|
|
|
36
47
|
'</div>';
|
|
37
48
|
|
|
38
49
|
// Load data
|
|
39
|
-
await Promise.all([loadIssueData(), loadQueueData()]);
|
|
50
|
+
await Promise.all([loadIssueData(), loadQueueData(), loadAllQueues()]);
|
|
40
51
|
|
|
41
52
|
// Render the main view
|
|
42
53
|
renderIssueView();
|
|
@@ -82,6 +93,20 @@ async function loadQueueData() {
|
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
async function loadAllQueues() {
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch('/api/queue/history?path=' + encodeURIComponent(projectPath));
|
|
99
|
+
if (!response.ok) throw new Error('Failed to load queue history');
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
queueData.queues = data.queues || [];
|
|
102
|
+
queueData.activeQueueId = data.active_queue_id;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('Failed to load all queues:', err);
|
|
105
|
+
queueData.queues = [];
|
|
106
|
+
queueData.activeQueueId = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
async function loadIssueDetail(issueId) {
|
|
86
111
|
try {
|
|
87
112
|
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath));
|
|
@@ -124,11 +149,24 @@ function renderIssueView() {
|
|
|
124
149
|
|
|
125
150
|
if (issueData.searchQuery) {
|
|
126
151
|
const query = issueData.searchQuery.toLowerCase();
|
|
127
|
-
filteredIssues = filteredIssues.filter(i =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
filteredIssues = filteredIssues.filter(i => {
|
|
153
|
+
// Basic field search
|
|
154
|
+
const basicMatch =
|
|
155
|
+
i.id.toLowerCase().includes(query) ||
|
|
156
|
+
(i.title && i.title.toLowerCase().includes(query)) ||
|
|
157
|
+
(i.context && i.context.toLowerCase().includes(query));
|
|
158
|
+
|
|
159
|
+
if (basicMatch) return true;
|
|
160
|
+
|
|
161
|
+
// Search in solutions
|
|
162
|
+
if (i.solutions && i.solutions.length > 0) {
|
|
163
|
+
return i.solutions.some(sol =>
|
|
164
|
+
(sol.description && sol.description.toLowerCase().includes(query)) ||
|
|
165
|
+
(sol.approach && sol.approach.toLowerCase().includes(query))
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
});
|
|
132
170
|
}
|
|
133
171
|
|
|
134
172
|
container.innerHTML = `
|
|
@@ -273,12 +311,18 @@ function renderIssueListSection(issues) {
|
|
|
273
311
|
id="issueSearchInput"
|
|
274
312
|
placeholder="${t('issues.searchPlaceholder') || 'Search issues...'}"
|
|
275
313
|
value="${issueData.searchQuery}"
|
|
276
|
-
oninput="handleIssueSearch(this.value)"
|
|
314
|
+
oninput="handleIssueSearch(this.value)"
|
|
315
|
+
onkeydown="handleSearchKeydown(event)"
|
|
316
|
+
onfocus="showSearchSuggestions()"
|
|
317
|
+
autocomplete="off" />
|
|
277
318
|
${issueData.searchQuery ? `
|
|
278
319
|
<button class="issue-search-clear" onclick="clearIssueSearch()">
|
|
279
320
|
<i data-lucide="x" class="w-3 h-3"></i>
|
|
280
321
|
</button>
|
|
281
322
|
` : ''}
|
|
323
|
+
<div class="search-suggestions ${issueData.showSuggestions && issueData.searchSuggestions.length > 0 ? 'show' : ''}" id="searchSuggestions">
|
|
324
|
+
${renderSearchSuggestions()}
|
|
325
|
+
</div>
|
|
282
326
|
</div>
|
|
283
327
|
|
|
284
328
|
<div class="issue-filters">
|
|
@@ -339,7 +383,7 @@ function renderIssueCard(issue) {
|
|
|
339
383
|
<div class="issue-card ${isArchived ? 'archived' : ''}" onclick="openIssueDetail('${issue.id}'${isArchived ? ', true' : ''})">
|
|
340
384
|
<div class="flex items-start justify-between mb-3">
|
|
341
385
|
<div class="flex items-center gap-2">
|
|
342
|
-
<span class="issue-id font-mono text-sm">${issue.id}</span>
|
|
386
|
+
<span class="issue-id font-mono text-sm">${highlightMatch(issue.id, issueData.searchQuery)}</span>
|
|
343
387
|
<span class="issue-status ${statusColors[issue.status] || ''}">${issue.status || 'unknown'}</span>
|
|
344
388
|
${isArchived ? '<span class="issue-archived-badge">' + (t('issues.archived') || 'Archived') + '</span>' : ''}
|
|
345
389
|
</div>
|
|
@@ -348,7 +392,7 @@ function renderIssueCard(issue) {
|
|
|
348
392
|
</span>
|
|
349
393
|
</div>
|
|
350
394
|
|
|
351
|
-
<h3 class="issue-title text-foreground font-medium mb-2">${issue.title || issue.id}</h3>
|
|
395
|
+
<h3 class="issue-title text-foreground font-medium mb-2">${highlightMatch(issue.title || issue.id, issueData.searchQuery)}</h3>
|
|
352
396
|
|
|
353
397
|
<div class="issue-meta flex items-center gap-4 text-sm text-muted-foreground">
|
|
354
398
|
<span class="flex items-center gap-1">
|
|
@@ -398,25 +442,57 @@ async function filterIssuesByStatus(status) {
|
|
|
398
442
|
|
|
399
443
|
// ========== Queue Section ==========
|
|
400
444
|
function renderQueueSection() {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
445
|
+
const queues = queueData.queues || [];
|
|
446
|
+
const activeQueueId = queueData.activeQueueId;
|
|
447
|
+
const expandedQueueId = queueData.expandedQueueId;
|
|
448
|
+
|
|
449
|
+
// If a queue is expanded, show loading then load detail
|
|
450
|
+
if (expandedQueueId) {
|
|
451
|
+
// Show loading state first, then load async
|
|
452
|
+
setTimeout(() => loadAndRenderExpandedQueue(expandedQueueId), 0);
|
|
409
453
|
return `
|
|
410
|
-
<div class="queue-
|
|
411
|
-
<div class="queue-
|
|
412
|
-
<button class="btn-secondary" onclick="
|
|
413
|
-
<i data-lucide="
|
|
414
|
-
<span>${t('
|
|
454
|
+
<div id="queueExpandedWrapper" class="queue-expanded-wrapper">
|
|
455
|
+
<div class="queue-detail-header mb-4">
|
|
456
|
+
<button class="btn-secondary" onclick="queueData.expandedQueueId = null; renderIssueView();">
|
|
457
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
458
|
+
<span>${t('common.back') || 'Back'}</span>
|
|
415
459
|
</button>
|
|
460
|
+
<div class="queue-detail-title">
|
|
461
|
+
<h3 class="font-mono text-lg">${escapeHtml(expandedQueueId)}</h3>
|
|
462
|
+
</div>
|
|
416
463
|
</div>
|
|
464
|
+
<div id="expandedQueueContent" class="flex items-center justify-center py-8">
|
|
465
|
+
<i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
|
|
466
|
+
<span class="ml-2">${t('common.loading') || 'Loading...'}</span>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Show multi-queue cards view
|
|
473
|
+
return `
|
|
474
|
+
<!-- Queue Cards Header -->
|
|
475
|
+
<div class="queue-cards-header mb-4">
|
|
476
|
+
<div class="flex items-center gap-3">
|
|
477
|
+
<h3 class="text-lg font-semibold">${t('issues.executionQueues') || 'Execution Queues'}</h3>
|
|
478
|
+
<span class="text-sm text-muted-foreground">${queues.length} ${t('issues.queues') || 'queues'}</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="flex items-center gap-2">
|
|
481
|
+
<button class="btn-secondary" onclick="loadAllQueues().then(() => renderIssueView())" title="${t('issues.refresh') || 'Refresh'}">
|
|
482
|
+
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
|
483
|
+
</button>
|
|
484
|
+
<button class="btn-primary" onclick="createExecutionQueue()">
|
|
485
|
+
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
486
|
+
<span>${t('issues.createQueue') || 'Create Queue'}</span>
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
${queues.length === 0 ? `
|
|
492
|
+
<div class="queue-empty-container">
|
|
417
493
|
<div class="queue-empty">
|
|
418
494
|
<i data-lucide="git-branch" class="w-16 h-16"></i>
|
|
419
|
-
<p class="queue-empty-title">${t('issues.
|
|
495
|
+
<p class="queue-empty-title">${t('issues.noQueues') || 'No queues found'}</p>
|
|
420
496
|
<p class="queue-empty-hint">${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}</p>
|
|
421
497
|
<button class="queue-create-btn" onclick="createExecutionQueue()">
|
|
422
498
|
<i data-lucide="play" class="w-4 h-4"></i>
|
|
@@ -424,14 +500,509 @@ function renderQueueSection() {
|
|
|
424
500
|
</button>
|
|
425
501
|
</div>
|
|
426
502
|
</div>
|
|
503
|
+
` : `
|
|
504
|
+
<!-- Queue Cards Grid -->
|
|
505
|
+
<div class="queue-cards-grid">
|
|
506
|
+
${queues.map(q => renderQueueCard(q, q.id === activeQueueId)).join('')}
|
|
507
|
+
</div>
|
|
508
|
+
`}
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderQueueCard(queue, isActive) {
|
|
513
|
+
const itemCount = queue.total_solutions || queue.total_tasks || 0;
|
|
514
|
+
const completedCount = queue.completed_solutions || queue.completed_tasks || 0;
|
|
515
|
+
const progressPercent = itemCount > 0 ? Math.round((completedCount / itemCount) * 100) : 0;
|
|
516
|
+
const issueCount = queue.issue_ids?.length || 0;
|
|
517
|
+
const statusClass = queue.status === 'merged' ? 'merged' : queue.status || '';
|
|
518
|
+
const safeQueueId = escapeHtml(queue.id || '');
|
|
519
|
+
|
|
520
|
+
return `
|
|
521
|
+
<div class="queue-card ${isActive ? 'active' : ''} ${statusClass}" onclick="toggleQueueExpand('${safeQueueId}')">
|
|
522
|
+
<div class="queue-card-header">
|
|
523
|
+
<span class="queue-card-id font-mono">${safeQueueId}</span>
|
|
524
|
+
<div class="queue-card-badges">
|
|
525
|
+
${isActive ? '<span class="queue-active-badge">Active</span>' : ''}
|
|
526
|
+
<span class="queue-status-badge ${statusClass}">${queue.status || 'unknown'}</span>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<div class="queue-card-stats">
|
|
531
|
+
<div class="progress-bar">
|
|
532
|
+
<div class="progress-fill ${queue.status === 'completed' ? 'completed' : ''}" style="width: ${progressPercent}%"></div>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="queue-card-progress">
|
|
535
|
+
<span>${completedCount}/${itemCount} ${queue.total_solutions ? 'solutions' : 'tasks'}</span>
|
|
536
|
+
<span class="text-muted-foreground">${progressPercent}%</span>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div class="queue-card-meta">
|
|
541
|
+
<span class="flex items-center gap-1">
|
|
542
|
+
<i data-lucide="layers" class="w-3 h-3"></i>
|
|
543
|
+
${issueCount} issues
|
|
544
|
+
</span>
|
|
545
|
+
<span class="flex items-center gap-1">
|
|
546
|
+
<i data-lucide="calendar" class="w-3 h-3"></i>
|
|
547
|
+
${queue.created_at ? new Date(queue.created_at).toLocaleDateString() : 'N/A'}
|
|
548
|
+
</span>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<div class="queue-card-actions" onclick="event.stopPropagation()">
|
|
552
|
+
<button class="btn-sm" onclick="toggleQueueExpand('${safeQueueId}')" title="View details">
|
|
553
|
+
<i data-lucide="eye" class="w-3 h-3"></i>
|
|
554
|
+
</button>
|
|
555
|
+
${!isActive && queue.status !== 'merged' ? `
|
|
556
|
+
<button class="btn-sm btn-primary" onclick="activateQueue('${safeQueueId}')" title="Set as active">
|
|
557
|
+
<i data-lucide="check-circle" class="w-3 h-3"></i>
|
|
558
|
+
</button>
|
|
559
|
+
` : ''}
|
|
560
|
+
${queue.status !== 'merged' ? `
|
|
561
|
+
<button class="btn-sm" onclick="showMergeQueueModal('${safeQueueId}')" title="Merge into another queue">
|
|
562
|
+
<i data-lucide="git-merge" class="w-3 h-3"></i>
|
|
563
|
+
</button>
|
|
564
|
+
` : ''}
|
|
565
|
+
<button class="btn-sm btn-danger" onclick="confirmDeleteQueue('${safeQueueId}')" title="${t('issues.deleteQueue') || 'Delete queue'}">
|
|
566
|
+
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
|
567
|
+
</button>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function toggleQueueExpand(queueId) {
|
|
574
|
+
if (queueData.expandedQueueId === queueId) {
|
|
575
|
+
queueData.expandedQueueId = null;
|
|
576
|
+
} else {
|
|
577
|
+
queueData.expandedQueueId = queueId;
|
|
578
|
+
}
|
|
579
|
+
renderIssueView();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function activateQueue(queueId) {
|
|
583
|
+
try {
|
|
584
|
+
const response = await fetch('/api/queue/switch?path=' + encodeURIComponent(projectPath), {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: { 'Content-Type': 'application/json' },
|
|
587
|
+
body: JSON.stringify({ queueId })
|
|
588
|
+
});
|
|
589
|
+
const result = await response.json();
|
|
590
|
+
if (result.success) {
|
|
591
|
+
showNotification(t('issues.queueActivated') || 'Queue activated: ' + queueId, 'success');
|
|
592
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
593
|
+
renderIssueView();
|
|
594
|
+
} else {
|
|
595
|
+
showNotification(result.error || 'Failed to activate queue', 'error');
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
console.error('Failed to activate queue:', err);
|
|
599
|
+
showNotification('Failed to activate queue', 'error');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function deactivateQueue(queueId) {
|
|
604
|
+
try {
|
|
605
|
+
const response = await fetch('/api/queue/deactivate?path=' + encodeURIComponent(projectPath), {
|
|
606
|
+
method: 'POST',
|
|
607
|
+
headers: { 'Content-Type': 'application/json' },
|
|
608
|
+
body: JSON.stringify({ queueId })
|
|
609
|
+
});
|
|
610
|
+
const result = await response.json();
|
|
611
|
+
if (result.success) {
|
|
612
|
+
showNotification(t('issues.queueDeactivated') || 'Queue deactivated', 'success');
|
|
613
|
+
queueData.activeQueueId = null;
|
|
614
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
615
|
+
renderIssueView();
|
|
616
|
+
} else {
|
|
617
|
+
showNotification(result.error || 'Failed to deactivate queue', 'error');
|
|
618
|
+
}
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.error('Failed to deactivate queue:', err);
|
|
621
|
+
showNotification('Failed to deactivate queue', 'error');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function confirmDeleteQueue(queueId) {
|
|
626
|
+
const msg = t('issues.confirmDeleteQueue') || 'Are you sure you want to delete this queue? This action cannot be undone.';
|
|
627
|
+
if (confirm(msg)) {
|
|
628
|
+
deleteQueue(queueId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function deleteQueue(queueId) {
|
|
633
|
+
try {
|
|
634
|
+
const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath), {
|
|
635
|
+
method: 'DELETE'
|
|
636
|
+
});
|
|
637
|
+
const result = await response.json();
|
|
638
|
+
if (result.success) {
|
|
639
|
+
showNotification(t('issues.queueDeleted') || 'Queue deleted successfully', 'success');
|
|
640
|
+
queueData.expandedQueueId = null;
|
|
641
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
642
|
+
renderIssueView();
|
|
643
|
+
} else {
|
|
644
|
+
showNotification(result.error || 'Failed to delete queue', 'error');
|
|
645
|
+
}
|
|
646
|
+
} catch (err) {
|
|
647
|
+
console.error('Failed to delete queue:', err);
|
|
648
|
+
showNotification('Failed to delete queue', 'error');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function renderExpandedQueueView(queueId) {
|
|
653
|
+
const safeQueueId = escapeHtml(queueId || '');
|
|
654
|
+
// Fetch queue detail
|
|
655
|
+
let queue;
|
|
656
|
+
try {
|
|
657
|
+
const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath));
|
|
658
|
+
queue = await response.json();
|
|
659
|
+
if (queue.error) throw new Error(queue.error);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
return `
|
|
662
|
+
<div class="queue-error">
|
|
663
|
+
<button class="btn-secondary mb-4" onclick="queueData.expandedQueueId = null; renderIssueView();">
|
|
664
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i> Back
|
|
665
|
+
</button>
|
|
666
|
+
<p class="text-red-500">Failed to load queue: ${escapeHtml(err.message)}</p>
|
|
667
|
+
</div>
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const queueItems = queue.solutions || queue.tasks || [];
|
|
672
|
+
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
|
673
|
+
const metadata = queue._metadata || {};
|
|
674
|
+
const isActive = queueId === queueData.activeQueueId;
|
|
675
|
+
|
|
676
|
+
// Group items by execution_group
|
|
677
|
+
const groupMap = {};
|
|
678
|
+
queueItems.forEach(item => {
|
|
679
|
+
const groupId = item.execution_group || 'default';
|
|
680
|
+
if (!groupMap[groupId]) groupMap[groupId] = [];
|
|
681
|
+
groupMap[groupId].push(item);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const groups = queue.execution_groups || Object.keys(groupMap).map(groupId => ({
|
|
685
|
+
id: groupId,
|
|
686
|
+
type: groupId.startsWith('P') ? 'parallel' : 'sequential',
|
|
687
|
+
solution_count: groupMap[groupId]?.length || 0
|
|
688
|
+
}));
|
|
689
|
+
const groupedItems = queue.grouped_items || groupMap;
|
|
690
|
+
|
|
691
|
+
return `
|
|
692
|
+
<!-- Back Button & Queue Header -->
|
|
693
|
+
<div class="queue-detail-header mb-4">
|
|
694
|
+
<button class="btn-secondary" onclick="queueData.expandedQueueId = null; renderIssueView();">
|
|
695
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
696
|
+
<span>${t('common.back') || 'Back'}</span>
|
|
697
|
+
</button>
|
|
698
|
+
<div class="queue-detail-title">
|
|
699
|
+
<h3 class="font-mono text-lg">${escapeHtml(queue.id || queueId)}</h3>
|
|
700
|
+
<div class="flex items-center gap-2">
|
|
701
|
+
${isActive ? '<span class="queue-active-badge">Active</span>' : ''}
|
|
702
|
+
<span class="queue-status-badge ${escapeHtml(queue.status || '')}">${escapeHtml(queue.status || 'unknown')}</span>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
<div class="queue-detail-actions">
|
|
706
|
+
${!isActive && queue.status !== 'merged' ? `
|
|
707
|
+
<button class="btn-primary" onclick="activateQueue('${safeQueueId}')">
|
|
708
|
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
|
709
|
+
<span>${t('issues.activate') || 'Activate'}</span>
|
|
710
|
+
</button>
|
|
711
|
+
` : ''}
|
|
712
|
+
${isActive ? `
|
|
713
|
+
<button class="btn-secondary btn-warning" onclick="deactivateQueue('${safeQueueId}')">
|
|
714
|
+
<i data-lucide="x-circle" class="w-4 h-4"></i>
|
|
715
|
+
<span>${t('issues.deactivate') || 'Deactivate'}</span>
|
|
716
|
+
</button>
|
|
717
|
+
` : ''}
|
|
718
|
+
<button class="btn-secondary" onclick="refreshExpandedQueue('${safeQueueId}')">
|
|
719
|
+
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
|
720
|
+
</button>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<!-- Queue Stats -->
|
|
725
|
+
<div class="queue-stats-grid mb-4">
|
|
726
|
+
<div class="queue-stat-card">
|
|
727
|
+
<span class="queue-stat-value">${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}</span>
|
|
728
|
+
<span class="queue-stat-label">${isSolutionLevel ? 'Solutions' : 'Tasks'}</span>
|
|
729
|
+
</div>
|
|
730
|
+
<div class="queue-stat-card pending">
|
|
731
|
+
<span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
|
|
732
|
+
<span class="queue-stat-label">Pending</span>
|
|
733
|
+
</div>
|
|
734
|
+
<div class="queue-stat-card executing">
|
|
735
|
+
<span class="queue-stat-value">${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length}</span>
|
|
736
|
+
<span class="queue-stat-label">Executing</span>
|
|
737
|
+
</div>
|
|
738
|
+
<div class="queue-stat-card completed">
|
|
739
|
+
<span class="queue-stat-value">${isSolutionLevel ? (metadata.completed_solutions || 0) : (metadata.completed_tasks || queueItems.filter(i => i.status === 'completed').length)}</span>
|
|
740
|
+
<span class="queue-stat-label">Completed</span>
|
|
741
|
+
</div>
|
|
742
|
+
<div class="queue-stat-card failed">
|
|
743
|
+
<span class="queue-stat-value">${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length}</span>
|
|
744
|
+
<span class="queue-stat-label">Failed</span>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
|
|
748
|
+
<div class="queue-info mb-4">
|
|
749
|
+
<p class="text-sm text-muted-foreground">
|
|
750
|
+
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
|
751
|
+
${t('issues.reorderHint') || 'Drag items within a group to reorder. Click item to view details.'}
|
|
752
|
+
</p>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="queue-timeline">
|
|
756
|
+
${groups.map(group => renderQueueGroupWithDelete(group, groupedItems[group.id] || groupMap[group.id] || [], queueId)).join('')}
|
|
757
|
+
</div>
|
|
758
|
+
|
|
759
|
+
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Async loader for expanded queue view - renders into DOM container
|
|
764
|
+
async function loadAndRenderExpandedQueue(queueId) {
|
|
765
|
+
const wrapper = document.getElementById('queueExpandedWrapper');
|
|
766
|
+
if (!wrapper) return;
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const html = await renderExpandedQueueView(queueId);
|
|
770
|
+
wrapper.innerHTML = html;
|
|
771
|
+
// Re-init icons and drag-drop after DOM update
|
|
772
|
+
if (window.lucide) {
|
|
773
|
+
window.lucide.createIcons();
|
|
774
|
+
}
|
|
775
|
+
// Initialize drag-drop for queue items
|
|
776
|
+
initQueueDragDrop();
|
|
777
|
+
} catch (err) {
|
|
778
|
+
console.error('Failed to load expanded queue:', err);
|
|
779
|
+
wrapper.innerHTML = `
|
|
780
|
+
<div class="text-center py-8 text-red-500">
|
|
781
|
+
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
|
|
782
|
+
<p>Failed to load queue: ${escapeHtml(err.message || 'Unknown error')}</p>
|
|
783
|
+
<button class="btn-secondary mt-4" onclick="queueData.expandedQueueId = null; renderIssueView();">
|
|
784
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i> Back
|
|
785
|
+
</button>
|
|
786
|
+
</div>
|
|
427
787
|
`;
|
|
788
|
+
if (window.lucide) {
|
|
789
|
+
window.lucide.createIcons();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function renderQueueGroupWithDelete(group, items, queueId) {
|
|
795
|
+
const isParallel = group.type === 'parallel';
|
|
796
|
+
const itemCount = group.solution_count || group.task_count || items.length;
|
|
797
|
+
const itemLabel = group.solution_count ? 'solutions' : 'tasks';
|
|
798
|
+
|
|
799
|
+
return `
|
|
800
|
+
<div class="queue-group" data-group-id="${group.id}">
|
|
801
|
+
<div class="queue-group-header">
|
|
802
|
+
<div class="queue-group-type ${isParallel ? 'parallel' : 'sequential'}">
|
|
803
|
+
<i data-lucide="${isParallel ? 'git-merge' : 'arrow-right'}" class="w-4 h-4"></i>
|
|
804
|
+
${group.id} (${isParallel ? 'Parallel' : 'Sequential'})
|
|
805
|
+
</div>
|
|
806
|
+
<span class="text-sm text-muted-foreground">${itemCount} ${itemLabel}</span>
|
|
807
|
+
</div>
|
|
808
|
+
<div class="queue-items ${isParallel ? 'parallel' : 'sequential'}">
|
|
809
|
+
${items.map((item, idx) => renderQueueItemWithDelete(item, idx, items.length, queueId)).join('')}
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderQueueItemWithDelete(item, index, total, queueId) {
|
|
816
|
+
const statusColors = {
|
|
817
|
+
pending: '',
|
|
818
|
+
ready: 'ready',
|
|
819
|
+
executing: 'executing',
|
|
820
|
+
completed: 'completed',
|
|
821
|
+
failed: 'failed',
|
|
822
|
+
blocked: 'blocked'
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const isSolutionItem = item.task_count !== undefined;
|
|
826
|
+
const safeItemId = escapeHtml(item.item_id || '');
|
|
827
|
+
const safeIssueId = escapeHtml(item.issue_id || '');
|
|
828
|
+
const safeQueueId = escapeHtml(queueId || '');
|
|
829
|
+
const safeSolutionId = escapeHtml(item.solution_id || '');
|
|
830
|
+
const safeTaskId = escapeHtml(item.task_id || '-');
|
|
831
|
+
const safeFilesTouched = item.files_touched ? escapeHtml(item.files_touched.join(', ')) : '';
|
|
832
|
+
const safeDependsOn = item.depends_on ? escapeHtml(item.depends_on.join(', ')) : '';
|
|
833
|
+
|
|
834
|
+
return `
|
|
835
|
+
<div class="queue-item ${statusColors[item.status] || ''}"
|
|
836
|
+
draggable="true"
|
|
837
|
+
data-item-id="${safeItemId}"
|
|
838
|
+
data-group-id="${escapeHtml(item.execution_group || '')}"
|
|
839
|
+
onclick="openQueueItemDetail('${safeItemId}')">
|
|
840
|
+
<span class="queue-item-id font-mono text-xs">${safeItemId}</span>
|
|
841
|
+
<span class="queue-item-issue text-xs text-muted-foreground">${safeIssueId}</span>
|
|
842
|
+
${isSolutionItem ? `
|
|
843
|
+
<span class="queue-item-solution text-sm" title="${safeSolutionId}">
|
|
844
|
+
<i data-lucide="package" class="w-3 h-3 inline mr-1"></i>
|
|
845
|
+
${item.task_count} tasks
|
|
846
|
+
</span>
|
|
847
|
+
${item.files_touched && item.files_touched.length > 0 ? `
|
|
848
|
+
<span class="queue-item-files text-xs text-muted-foreground" title="${safeFilesTouched}">
|
|
849
|
+
<i data-lucide="file" class="w-3 h-3"></i>
|
|
850
|
+
${item.files_touched.length}
|
|
851
|
+
</span>
|
|
852
|
+
` : ''}
|
|
853
|
+
` : `
|
|
854
|
+
<span class="queue-item-task text-sm">${safeTaskId}</span>
|
|
855
|
+
`}
|
|
856
|
+
<span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
|
|
857
|
+
<i data-lucide="arrow-up" class="w-3 h-3"></i>
|
|
858
|
+
</span>
|
|
859
|
+
${item.depends_on && item.depends_on.length > 0 ? `
|
|
860
|
+
<span class="queue-item-deps text-xs text-muted-foreground" title="Depends on: ${safeDependsOn}">
|
|
861
|
+
<i data-lucide="link" class="w-3 h-3"></i>
|
|
862
|
+
</span>
|
|
863
|
+
` : ''}
|
|
864
|
+
<button class="queue-item-delete btn-icon" onclick="event.stopPropagation(); deleteQueueItem('${safeQueueId}', '${safeItemId}')" title="Delete item">
|
|
865
|
+
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
|
866
|
+
</button>
|
|
867
|
+
</div>
|
|
868
|
+
`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function deleteQueueItem(queueId, itemId) {
|
|
872
|
+
if (!confirm('Delete this item from queue?')) return;
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
const response = await fetch('/api/queue/' + queueId + '/item/' + encodeURIComponent(itemId) + '?path=' + encodeURIComponent(projectPath), {
|
|
876
|
+
method: 'DELETE'
|
|
877
|
+
});
|
|
878
|
+
const result = await response.json();
|
|
879
|
+
|
|
880
|
+
if (result.success) {
|
|
881
|
+
showNotification('Item deleted from queue', 'success');
|
|
882
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
883
|
+
renderIssueView();
|
|
884
|
+
} else {
|
|
885
|
+
showNotification(result.error || 'Failed to delete item', 'error');
|
|
886
|
+
}
|
|
887
|
+
} catch (err) {
|
|
888
|
+
console.error('Failed to delete queue item:', err);
|
|
889
|
+
showNotification('Failed to delete item', 'error');
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function refreshExpandedQueue(queueId) {
|
|
894
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
895
|
+
renderIssueView();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ========== Queue Merge Modal ==========
|
|
899
|
+
function showMergeQueueModal(sourceQueueId) {
|
|
900
|
+
let modal = document.getElementById('mergeQueueModal');
|
|
901
|
+
if (!modal) {
|
|
902
|
+
modal = document.createElement('div');
|
|
903
|
+
modal.id = 'mergeQueueModal';
|
|
904
|
+
modal.className = 'issue-modal';
|
|
905
|
+
document.body.appendChild(modal);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const otherQueues = queueData.queues.filter(q =>
|
|
909
|
+
q.id !== sourceQueueId && q.status !== 'merged'
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const safeSourceId = escapeHtml(sourceQueueId || '');
|
|
913
|
+
|
|
914
|
+
modal.innerHTML = `
|
|
915
|
+
<div class="issue-modal-backdrop" onclick="hideMergeQueueModal()"></div>
|
|
916
|
+
<div class="issue-modal-content" style="max-width: 500px;">
|
|
917
|
+
<div class="issue-modal-header">
|
|
918
|
+
<h3><i data-lucide="git-merge" class="w-5 h-5 inline mr-2"></i>Merge Queue</h3>
|
|
919
|
+
<button class="btn-icon" onclick="hideMergeQueueModal()">
|
|
920
|
+
<i data-lucide="x" class="w-5 h-5"></i>
|
|
921
|
+
</button>
|
|
922
|
+
</div>
|
|
923
|
+
<div class="issue-modal-body">
|
|
924
|
+
<p class="mb-4">Merge <strong class="font-mono">${safeSourceId}</strong> into another queue:</p>
|
|
925
|
+
${otherQueues.length === 0 ? `
|
|
926
|
+
<p class="text-muted-foreground text-center py-4">No other queues available for merging</p>
|
|
927
|
+
` : `
|
|
928
|
+
<div class="form-group">
|
|
929
|
+
<label>Target Queue</label>
|
|
930
|
+
<select id="targetQueueSelect" class="w-full">
|
|
931
|
+
${otherQueues.map(q => `
|
|
932
|
+
<option value="${escapeHtml(q.id)}">${escapeHtml(q.id)} (${q.total_solutions || q.total_tasks || 0} items)</option>
|
|
933
|
+
`).join('')}
|
|
934
|
+
</select>
|
|
935
|
+
</div>
|
|
936
|
+
<p class="text-sm text-muted-foreground mt-2">
|
|
937
|
+
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
|
938
|
+
Items from source queue will be appended to target queue. Source queue will be marked as "merged".
|
|
939
|
+
</p>
|
|
940
|
+
`}
|
|
941
|
+
</div>
|
|
942
|
+
<div class="issue-modal-footer">
|
|
943
|
+
<button class="btn-secondary" onclick="hideMergeQueueModal()">Cancel</button>
|
|
944
|
+
${otherQueues.length > 0 ? `
|
|
945
|
+
<button class="btn-primary" onclick="executeQueueMerge('${safeSourceId}')">
|
|
946
|
+
<i data-lucide="git-merge" class="w-4 h-4"></i>
|
|
947
|
+
Merge
|
|
948
|
+
</button>
|
|
949
|
+
` : ''}
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
`;
|
|
953
|
+
|
|
954
|
+
modal.classList.remove('hidden');
|
|
955
|
+
lucide.createIcons();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function hideMergeQueueModal() {
|
|
959
|
+
const modal = document.getElementById('mergeQueueModal');
|
|
960
|
+
if (modal) {
|
|
961
|
+
modal.classList.add('hidden');
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async function executeQueueMerge(sourceQueueId) {
|
|
966
|
+
const targetQueueId = document.getElementById('targetQueueSelect')?.value;
|
|
967
|
+
if (!targetQueueId) return;
|
|
968
|
+
|
|
969
|
+
try {
|
|
970
|
+
const response = await fetch('/api/queue/merge?path=' + encodeURIComponent(projectPath), {
|
|
971
|
+
method: 'POST',
|
|
972
|
+
headers: { 'Content-Type': 'application/json' },
|
|
973
|
+
body: JSON.stringify({ sourceQueueId, targetQueueId })
|
|
974
|
+
});
|
|
975
|
+
const result = await response.json();
|
|
976
|
+
|
|
977
|
+
if (result.success) {
|
|
978
|
+
showNotification('Merged ' + result.mergedItemCount + ' items into ' + targetQueueId, 'success');
|
|
979
|
+
hideMergeQueueModal();
|
|
980
|
+
queueData.expandedQueueId = null;
|
|
981
|
+
await Promise.all([loadQueueData(), loadAllQueues()]);
|
|
982
|
+
renderIssueView();
|
|
983
|
+
} else {
|
|
984
|
+
showNotification(result.error || 'Failed to merge queues', 'error');
|
|
985
|
+
}
|
|
986
|
+
} catch (err) {
|
|
987
|
+
console.error('Failed to merge queues:', err);
|
|
988
|
+
showNotification('Failed to merge queues', 'error');
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ========== Legacy Queue Render (for backward compatibility) ==========
|
|
993
|
+
function renderLegacyQueueSection() {
|
|
994
|
+
const queue = issueData.queue;
|
|
995
|
+
const queueItems = queue.solutions || queue.tasks || [];
|
|
996
|
+
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
|
997
|
+
const metadata = queue._metadata || {};
|
|
998
|
+
|
|
999
|
+
if (queueItems.length === 0) {
|
|
1000
|
+
return `<div class="queue-empty"><p>Queue is empty</p></div>`;
|
|
428
1001
|
}
|
|
429
1002
|
|
|
430
|
-
// Group items by execution_group or treat all as single group
|
|
431
1003
|
const groups = queue.execution_groups || [];
|
|
432
1004
|
let groupedItems = queue.grouped_items || {};
|
|
433
1005
|
|
|
434
|
-
// If no execution_groups, create a default grouping from queue items
|
|
435
1006
|
if (groups.length === 0 && queueItems.length > 0) {
|
|
436
1007
|
const groupMap = {};
|
|
437
1008
|
queueItems.forEach(item => {
|
|
@@ -442,7 +1013,6 @@ function renderQueueSection() {
|
|
|
442
1013
|
groupMap[groupId].push(item);
|
|
443
1014
|
});
|
|
444
1015
|
|
|
445
|
-
// Create synthetic groups
|
|
446
1016
|
const syntheticGroups = Object.keys(groupMap).map(groupId => ({
|
|
447
1017
|
id: groupId,
|
|
448
1018
|
type: 'sequential',
|
|
@@ -450,7 +1020,6 @@ function renderQueueSection() {
|
|
|
450
1020
|
}));
|
|
451
1021
|
|
|
452
1022
|
return `
|
|
453
|
-
<!-- Queue Header -->
|
|
454
1023
|
<div class="queue-toolbar mb-4">
|
|
455
1024
|
<div class="queue-stats">
|
|
456
1025
|
<div class="queue-info-card">
|
|
@@ -866,6 +1435,23 @@ function renderIssueDetailPanel(issue) {
|
|
|
866
1435
|
`).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noTasks') || 'No tasks') + '</p>'}
|
|
867
1436
|
</div>
|
|
868
1437
|
</div>
|
|
1438
|
+
|
|
1439
|
+
<!-- Actions -->
|
|
1440
|
+
<div class="detail-section issue-detail-actions">
|
|
1441
|
+
<label class="detail-label">${t('issues.actions') || 'Actions'}</label>
|
|
1442
|
+
<div class="flex gap-2 flex-wrap">
|
|
1443
|
+
${!issue._isArchived ? `
|
|
1444
|
+
<button class="btn-secondary btn-sm" onclick="confirmArchiveIssue('${issue.id}')">
|
|
1445
|
+
<i data-lucide="archive" class="w-4 h-4"></i>
|
|
1446
|
+
${t('issues.archive') || 'Archive'}
|
|
1447
|
+
</button>
|
|
1448
|
+
` : ''}
|
|
1449
|
+
<button class="btn-secondary btn-sm btn-danger" onclick="confirmDeleteIssue('${issue.id}', ${issue._isArchived || false})">
|
|
1450
|
+
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
1451
|
+
${t('issues.delete') || 'Delete'}
|
|
1452
|
+
</button>
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
869
1455
|
</div>
|
|
870
1456
|
`;
|
|
871
1457
|
|
|
@@ -880,6 +1466,67 @@ function closeIssueDetail() {
|
|
|
880
1466
|
issueData.selectedIssue = null;
|
|
881
1467
|
}
|
|
882
1468
|
|
|
1469
|
+
// ========== Issue Delete & Archive ==========
|
|
1470
|
+
function confirmDeleteIssue(issueId, isArchived) {
|
|
1471
|
+
const msg = t('issues.confirmDeleteIssue') || 'Are you sure you want to delete this issue? This action cannot be undone.';
|
|
1472
|
+
if (confirm(msg)) {
|
|
1473
|
+
deleteIssue(issueId, isArchived);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function deleteIssue(issueId, isArchived) {
|
|
1478
|
+
try {
|
|
1479
|
+
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
|
|
1480
|
+
method: 'DELETE'
|
|
1481
|
+
});
|
|
1482
|
+
const result = await response.json();
|
|
1483
|
+
if (result.success) {
|
|
1484
|
+
showNotification(t('issues.issueDeleted') || 'Issue deleted successfully', 'success');
|
|
1485
|
+
closeIssueDetail();
|
|
1486
|
+
if (isArchived) {
|
|
1487
|
+
issueData.historyIssues = issueData.historyIssues.filter(i => i.id !== issueId);
|
|
1488
|
+
} else {
|
|
1489
|
+
issueData.issues = issueData.issues.filter(i => i.id !== issueId);
|
|
1490
|
+
}
|
|
1491
|
+
renderIssueView();
|
|
1492
|
+
updateIssueBadge();
|
|
1493
|
+
} else {
|
|
1494
|
+
showNotification(result.error || 'Failed to delete issue', 'error');
|
|
1495
|
+
}
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
console.error('Failed to delete issue:', err);
|
|
1498
|
+
showNotification('Failed to delete issue', 'error');
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function confirmArchiveIssue(issueId) {
|
|
1503
|
+
const msg = t('issues.confirmArchiveIssue') || 'Archive this issue? It will be moved to history.';
|
|
1504
|
+
if (confirm(msg)) {
|
|
1505
|
+
archiveIssue(issueId);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function archiveIssue(issueId) {
|
|
1510
|
+
try {
|
|
1511
|
+
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/archive?path=' + encodeURIComponent(projectPath), {
|
|
1512
|
+
method: 'POST'
|
|
1513
|
+
});
|
|
1514
|
+
const result = await response.json();
|
|
1515
|
+
if (result.success) {
|
|
1516
|
+
showNotification(t('issues.issueArchived') || 'Issue archived successfully', 'success');
|
|
1517
|
+
closeIssueDetail();
|
|
1518
|
+
await loadIssueData();
|
|
1519
|
+
renderIssueView();
|
|
1520
|
+
updateIssueBadge();
|
|
1521
|
+
} else {
|
|
1522
|
+
showNotification(result.error || 'Failed to archive issue', 'error');
|
|
1523
|
+
}
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
console.error('Failed to archive issue:', err);
|
|
1526
|
+
showNotification('Failed to archive issue', 'error');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
883
1530
|
function toggleSolutionExpand(solId) {
|
|
884
1531
|
const el = document.getElementById('solution-' + solId);
|
|
885
1532
|
if (el) {
|
|
@@ -1234,6 +1881,20 @@ function escapeHtml(text) {
|
|
|
1234
1881
|
return div.innerHTML;
|
|
1235
1882
|
}
|
|
1236
1883
|
|
|
1884
|
+
// Helper: escape regex special characters
|
|
1885
|
+
function escapeRegex(str) {
|
|
1886
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// Helper: highlight matching text in search results
|
|
1890
|
+
function highlightMatch(text, query) {
|
|
1891
|
+
if (!text || !query) return escapeHtml(text || '');
|
|
1892
|
+
const escaped = escapeHtml(text);
|
|
1893
|
+
const escapedQuery = escapeRegex(escapeHtml(query));
|
|
1894
|
+
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
1895
|
+
return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1237
1898
|
function openQueueItemDetail(itemId) {
|
|
1238
1899
|
// Support both solution-level and task-level queues
|
|
1239
1900
|
const items = issueData.queue.solutions || issueData.queue.tasks || [];
|
|
@@ -1366,16 +2027,178 @@ async function updateTaskStatus(issueId, taskId, status) {
|
|
|
1366
2027
|
}
|
|
1367
2028
|
|
|
1368
2029
|
// ========== Search Functions ==========
|
|
2030
|
+
var searchDebounceTimer = null;
|
|
2031
|
+
|
|
1369
2032
|
function handleIssueSearch(value) {
|
|
1370
2033
|
issueData.searchQuery = value;
|
|
1371
|
-
|
|
2034
|
+
|
|
2035
|
+
// Update suggestions immediately (no debounce for dropdown)
|
|
2036
|
+
updateSearchSuggestions(value);
|
|
2037
|
+
issueData.showSuggestions = value.length > 0;
|
|
2038
|
+
issueData.selectedSuggestion = -1;
|
|
2039
|
+
updateSuggestionsDropdown();
|
|
2040
|
+
|
|
2041
|
+
// Clear previous timer
|
|
2042
|
+
if (searchDebounceTimer) {
|
|
2043
|
+
clearTimeout(searchDebounceTimer);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// 300ms debounce for full re-render to prevent freeze on rapid input
|
|
2047
|
+
searchDebounceTimer = setTimeout(() => {
|
|
2048
|
+
renderIssueView();
|
|
2049
|
+
// Restore input focus and cursor position
|
|
2050
|
+
const input = document.getElementById('issueSearchInput');
|
|
2051
|
+
if (input) {
|
|
2052
|
+
input.focus();
|
|
2053
|
+
input.setSelectionRange(value.length, value.length);
|
|
2054
|
+
}
|
|
2055
|
+
}, 300);
|
|
1372
2056
|
}
|
|
1373
2057
|
|
|
1374
2058
|
function clearIssueSearch() {
|
|
2059
|
+
if (searchDebounceTimer) {
|
|
2060
|
+
clearTimeout(searchDebounceTimer);
|
|
2061
|
+
}
|
|
1375
2062
|
issueData.searchQuery = '';
|
|
2063
|
+
issueData.showSuggestions = false;
|
|
2064
|
+
issueData.searchSuggestions = [];
|
|
2065
|
+
issueData.selectedSuggestion = -1;
|
|
1376
2066
|
renderIssueView();
|
|
1377
2067
|
}
|
|
1378
2068
|
|
|
2069
|
+
// Update search suggestions based on query
|
|
2070
|
+
function updateSearchSuggestions(query) {
|
|
2071
|
+
if (!query || query.length < 1) {
|
|
2072
|
+
issueData.searchSuggestions = [];
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const q = query.toLowerCase();
|
|
2077
|
+
const allIssues = [...issueData.issues, ...issueData.historyIssues];
|
|
2078
|
+
|
|
2079
|
+
// Find matching issues (max 6)
|
|
2080
|
+
issueData.searchSuggestions = allIssues
|
|
2081
|
+
.filter(issue => {
|
|
2082
|
+
const idMatch = issue.id.toLowerCase().includes(q);
|
|
2083
|
+
const titleMatch = issue.title && issue.title.toLowerCase().includes(q);
|
|
2084
|
+
const contextMatch = issue.context && issue.context.toLowerCase().includes(q);
|
|
2085
|
+
const solutionMatch = issue.solutions && issue.solutions.some(sol =>
|
|
2086
|
+
(sol.description && sol.description.toLowerCase().includes(q)) ||
|
|
2087
|
+
(sol.approach && sol.approach.toLowerCase().includes(q))
|
|
2088
|
+
);
|
|
2089
|
+
return idMatch || titleMatch || contextMatch || solutionMatch;
|
|
2090
|
+
})
|
|
2091
|
+
.slice(0, 6);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// Render search suggestions dropdown
|
|
2095
|
+
function renderSearchSuggestions() {
|
|
2096
|
+
if (!issueData.searchSuggestions || issueData.searchSuggestions.length === 0) {
|
|
2097
|
+
return '';
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
return issueData.searchSuggestions.map((issue, index) => `
|
|
2101
|
+
<div class="search-suggestion-item ${index === issueData.selectedSuggestion ? 'selected' : ''}"
|
|
2102
|
+
onclick="selectSuggestion(${index})"
|
|
2103
|
+
onmouseenter="issueData.selectedSuggestion = ${index}">
|
|
2104
|
+
<div class="suggestion-id">${highlightMatch(issue.id, issueData.searchQuery)}</div>
|
|
2105
|
+
<div class="suggestion-title">${highlightMatch(issue.title || issue.id, issueData.searchQuery)}</div>
|
|
2106
|
+
</div>
|
|
2107
|
+
`).join('');
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// Show search suggestions
|
|
2111
|
+
function showSearchSuggestions() {
|
|
2112
|
+
if (issueData.searchQuery) {
|
|
2113
|
+
updateSearchSuggestions(issueData.searchQuery);
|
|
2114
|
+
issueData.showSuggestions = true;
|
|
2115
|
+
updateSuggestionsDropdown();
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Hide search suggestions
|
|
2120
|
+
function hideSearchSuggestions() {
|
|
2121
|
+
issueData.showSuggestions = false;
|
|
2122
|
+
issueData.selectedSuggestion = -1;
|
|
2123
|
+
const dropdown = document.getElementById('searchSuggestions');
|
|
2124
|
+
if (dropdown) {
|
|
2125
|
+
dropdown.classList.remove('show');
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Update suggestions dropdown without full re-render
|
|
2130
|
+
function updateSuggestionsDropdown() {
|
|
2131
|
+
const dropdown = document.getElementById('searchSuggestions');
|
|
2132
|
+
if (dropdown) {
|
|
2133
|
+
dropdown.innerHTML = renderSearchSuggestions();
|
|
2134
|
+
if (issueData.showSuggestions && issueData.searchSuggestions.length > 0) {
|
|
2135
|
+
dropdown.classList.add('show');
|
|
2136
|
+
} else {
|
|
2137
|
+
dropdown.classList.remove('show');
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// Select a suggestion
|
|
2143
|
+
function selectSuggestion(index) {
|
|
2144
|
+
const issue = issueData.searchSuggestions[index];
|
|
2145
|
+
if (issue) {
|
|
2146
|
+
hideSearchSuggestions();
|
|
2147
|
+
openIssueDetail(issue.id, issue._isArchived);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// Handle keyboard navigation in search
|
|
2152
|
+
function handleSearchKeydown(event) {
|
|
2153
|
+
const suggestions = issueData.searchSuggestions || [];
|
|
2154
|
+
|
|
2155
|
+
if (!issueData.showSuggestions || suggestions.length === 0) {
|
|
2156
|
+
// If Enter and no suggestions, just search
|
|
2157
|
+
if (event.key === 'Enter') {
|
|
2158
|
+
hideSearchSuggestions();
|
|
2159
|
+
}
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
switch (event.key) {
|
|
2164
|
+
case 'ArrowDown':
|
|
2165
|
+
event.preventDefault();
|
|
2166
|
+
issueData.selectedSuggestion = Math.min(
|
|
2167
|
+
issueData.selectedSuggestion + 1,
|
|
2168
|
+
suggestions.length - 1
|
|
2169
|
+
);
|
|
2170
|
+
updateSuggestionsDropdown();
|
|
2171
|
+
break;
|
|
2172
|
+
|
|
2173
|
+
case 'ArrowUp':
|
|
2174
|
+
event.preventDefault();
|
|
2175
|
+
issueData.selectedSuggestion = Math.max(issueData.selectedSuggestion - 1, -1);
|
|
2176
|
+
updateSuggestionsDropdown();
|
|
2177
|
+
break;
|
|
2178
|
+
|
|
2179
|
+
case 'Enter':
|
|
2180
|
+
event.preventDefault();
|
|
2181
|
+
if (issueData.selectedSuggestion >= 0) {
|
|
2182
|
+
selectSuggestion(issueData.selectedSuggestion);
|
|
2183
|
+
} else {
|
|
2184
|
+
hideSearchSuggestions();
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
|
|
2188
|
+
case 'Escape':
|
|
2189
|
+
hideSearchSuggestions();
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Close suggestions when clicking outside
|
|
2195
|
+
document.addEventListener('click', function(event) {
|
|
2196
|
+
const searchContainer = document.querySelector('.issue-search');
|
|
2197
|
+
if (searchContainer && !searchContainer.contains(event.target)) {
|
|
2198
|
+
hideSearchSuggestions();
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
|
|
1379
2202
|
// ========== Create Issue Modal ==========
|
|
1380
2203
|
function generateIssueId() {
|
|
1381
2204
|
// Generate unique ID: ISSUE-YYYYMMDD-XXX format
|