mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: 添加队列和议题删除功能,支持归档议题
This commit is contained in:
@@ -67,6 +67,12 @@ function readIssueHistoryJsonl(issuesDir: string): any[] {
|
||||
}
|
||||
}
|
||||
|
||||
function writeIssueHistoryJsonl(issuesDir: string, issues: any[]) {
|
||||
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
||||
const historyPath = join(issuesDir, 'issue-history.jsonl');
|
||||
writeFileSync(historyPath, issues.map(i => JSON.stringify(i)).join('\n'));
|
||||
}
|
||||
|
||||
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
|
||||
const solutionsDir = join(issuesDir, 'solutions');
|
||||
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
|
||||
@@ -556,6 +562,48 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/queue/:queueId - Delete entire queue
|
||||
const queueDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
||||
if (queueDeleteMatch && req.method === 'DELETE') {
|
||||
const queueId = queueDeleteMatch[1];
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
|
||||
if (!existsSync(queueFilePath)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete queue file
|
||||
unlinkSync(queueFilePath);
|
||||
|
||||
// Update index
|
||||
if (existsSync(indexPath)) {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
|
||||
// Remove from queues array
|
||||
index.queues = (index.queues || []).filter((q: any) => q.id !== queueId);
|
||||
|
||||
// Clear active if this was the active queue
|
||||
if (index.active_queue_id === queueId) {
|
||||
index.active_queue_id = null;
|
||||
}
|
||||
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, deletedQueueId: queueId }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to delete queue' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/merge - Merge source queue into target queue
|
||||
if (pathname === '/api/queue/merge' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
@@ -817,6 +865,39 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues/:id/archive - Archive issue (move to history)
|
||||
const archiveMatch = pathname.match(/^\/api\/issues\/([^/]+)\/archive$/);
|
||||
if (archiveMatch && req.method === 'POST') {
|
||||
const issueId = decodeURIComponent(archiveMatch[1]);
|
||||
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issueIndex = issues.findIndex(i => i.id === issueId);
|
||||
|
||||
if (issueIndex === -1) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Issue not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the issue and add archive metadata
|
||||
const issue = issues[issueIndex];
|
||||
issue.archived_at = new Date().toISOString();
|
||||
issue.status = 'completed';
|
||||
|
||||
// Move to history
|
||||
const history = readIssueHistoryJsonl(issuesDir);
|
||||
history.push(issue);
|
||||
writeIssueHistoryJsonl(issuesDir, history);
|
||||
|
||||
// Remove from active issues
|
||||
issues.splice(issueIndex, 1);
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, issueId, archivedAt: issue.archived_at }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues/:id/solutions - Add solution
|
||||
const addSolMatch = pathname.match(/^\/api\/issues\/([^/]+)\/solutions$/);
|
||||
if (addSolMatch && req.method === 'POST') {
|
||||
|
||||
@@ -3258,6 +3258,34 @@
|
||||
border-color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.btn-danger,
|
||||
.btn-secondary.btn-danger,
|
||||
.btn-sm.btn-danger {
|
||||
color: hsl(var(--destructive));
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
background: hsl(var(--destructive) / 0.08);
|
||||
}
|
||||
|
||||
.btn-danger:hover,
|
||||
.btn-secondary.btn-danger:hover,
|
||||
.btn-sm.btn-danger:hover {
|
||||
background: hsl(var(--destructive) / 0.15);
|
||||
border-color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Issue Detail Actions */
|
||||
.issue-detail-actions {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.issue-detail-actions .flex {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Active queue badge enhancement */
|
||||
.queue-active-badge {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -2273,6 +2273,16 @@ const i18n = {
|
||||
'issues.deactivate': 'Deactivate',
|
||||
'issues.queueActivated': 'Queue activated',
|
||||
'issues.queueDeactivated': 'Queue deactivated',
|
||||
'issues.deleteQueue': 'Delete queue',
|
||||
'issues.confirmDeleteQueue': 'Are you sure you want to delete this queue? This action cannot be undone.',
|
||||
'issues.queueDeleted': 'Queue deleted successfully',
|
||||
'issues.actions': 'Actions',
|
||||
'issues.archive': 'Archive',
|
||||
'issues.delete': 'Delete',
|
||||
'issues.confirmDeleteIssue': 'Are you sure you want to delete this issue? This action cannot be undone.',
|
||||
'issues.confirmArchiveIssue': 'Archive this issue? It will be moved to history.',
|
||||
'issues.issueDeleted': 'Issue deleted successfully',
|
||||
'issues.issueArchived': 'Issue archived successfully',
|
||||
'issues.executionQueues': 'Execution Queues',
|
||||
'issues.queues': 'queues',
|
||||
'issues.noQueues': 'No queues found',
|
||||
@@ -4605,6 +4615,16 @@ const i18n = {
|
||||
'issues.deactivate': '取消激活',
|
||||
'issues.queueActivated': '队列已激活',
|
||||
'issues.queueDeactivated': '队列已取消激活',
|
||||
'issues.deleteQueue': '删除队列',
|
||||
'issues.confirmDeleteQueue': '确定要删除此队列吗?此操作无法撤销。',
|
||||
'issues.queueDeleted': '队列删除成功',
|
||||
'issues.actions': '操作',
|
||||
'issues.archive': '归档',
|
||||
'issues.delete': '删除',
|
||||
'issues.confirmDeleteIssue': '确定要删除此议题吗?此操作无法撤销。',
|
||||
'issues.confirmArchiveIssue': '归档此议题?它将被移动到历史记录中。',
|
||||
'issues.issueDeleted': '议题删除成功',
|
||||
'issues.issueArchived': '议题归档成功',
|
||||
'issues.executionQueues': '执行队列',
|
||||
'issues.queues': '个队列',
|
||||
'issues.noQueues': '暂无队列',
|
||||
|
||||
@@ -562,6 +562,9 @@ function renderQueueCard(queue, isActive) {
|
||||
<i data-lucide="git-merge" class="w-3 h-3"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-sm btn-danger" onclick="confirmDeleteQueue('${safeQueueId}')" title="${t('issues.deleteQueue') || 'Delete queue'}">
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -619,6 +622,33 @@ async function deactivateQueue(queueId) {
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteQueue(queueId) {
|
||||
const msg = t('issues.confirmDeleteQueue') || 'Are you sure you want to delete this queue? This action cannot be undone.';
|
||||
if (confirm(msg)) {
|
||||
deleteQueue(queueId);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteQueue(queueId) {
|
||||
try {
|
||||
const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification(t('issues.queueDeleted') || 'Queue deleted successfully', 'success');
|
||||
queueData.expandedQueueId = null;
|
||||
await Promise.all([loadQueueData(), loadAllQueues()]);
|
||||
renderIssueView();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to delete queue', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete queue:', err);
|
||||
showNotification('Failed to delete queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function renderExpandedQueueView(queueId) {
|
||||
const safeQueueId = escapeHtml(queueId || '');
|
||||
// Fetch queue detail
|
||||
@@ -1405,6 +1435,23 @@ function renderIssueDetailPanel(issue) {
|
||||
`).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noTasks') || 'No tasks') + '</p>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="detail-section issue-detail-actions">
|
||||
<label class="detail-label">${t('issues.actions') || 'Actions'}</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
${!issue._isArchived ? `
|
||||
<button class="btn-secondary btn-sm" onclick="confirmArchiveIssue('${issue.id}')">
|
||||
<i data-lucide="archive" class="w-4 h-4"></i>
|
||||
${t('issues.archive') || 'Archive'}
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-secondary btn-sm btn-danger" onclick="confirmDeleteIssue('${issue.id}', ${issue._isArchived || false})">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('issues.delete') || 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1419,6 +1466,67 @@ function closeIssueDetail() {
|
||||
issueData.selectedIssue = null;
|
||||
}
|
||||
|
||||
// ========== Issue Delete & Archive ==========
|
||||
function confirmDeleteIssue(issueId, isArchived) {
|
||||
const msg = t('issues.confirmDeleteIssue') || 'Are you sure you want to delete this issue? This action cannot be undone.';
|
||||
if (confirm(msg)) {
|
||||
deleteIssue(issueId, isArchived);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIssue(issueId, isArchived) {
|
||||
try {
|
||||
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification(t('issues.issueDeleted') || 'Issue deleted successfully', 'success');
|
||||
closeIssueDetail();
|
||||
if (isArchived) {
|
||||
issueData.historyIssues = issueData.historyIssues.filter(i => i.id !== issueId);
|
||||
} else {
|
||||
issueData.issues = issueData.issues.filter(i => i.id !== issueId);
|
||||
}
|
||||
renderIssueView();
|
||||
updateIssueBadge();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to delete issue', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete issue:', err);
|
||||
showNotification('Failed to delete issue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmArchiveIssue(issueId) {
|
||||
const msg = t('issues.confirmArchiveIssue') || 'Archive this issue? It will be moved to history.';
|
||||
if (confirm(msg)) {
|
||||
archiveIssue(issueId);
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveIssue(issueId) {
|
||||
try {
|
||||
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/archive?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification(t('issues.issueArchived') || 'Issue archived successfully', 'success');
|
||||
closeIssueDetail();
|
||||
await loadIssueData();
|
||||
renderIssueView();
|
||||
updateIssueBadge();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to archive issue', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to archive issue:', err);
|
||||
showNotification('Failed to archive issue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSolutionExpand(solId) {
|
||||
const el = document.getElementById('solution-' + solId);
|
||||
if (el) {
|
||||
|
||||
Reference in New Issue
Block a user