Skip to content

Commit 0e6cd94

Browse files
authored
Merge pull request #56 from TeigenZhang/feat/case-manage-reorder-delete
feat: add case reorder and delete in Manage tab
2 parents 24a6f1c + c642689 commit 0e6cd94

7 files changed

Lines changed: 341 additions & 5 deletions

File tree

src/web/public/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ const SSE_EVENTS = {
291291
ORCHESTRATOR_TASK_FAILED: 'orchestrator:taskFailed',
292292
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
293293
ORCHESTRATOR_ERROR: 'orchestrator:error',
294+
295+
// Cases
296+
CASE_CREATED: 'case:created',
297+
CASE_LINKED: 'case:linked',
298+
CASE_DELETED: 'case:deleted',
299+
CASE_ORDER_CHANGED: 'case:order-changed',
294300
};
295301

296302
// ═══════════════════════════════════════════════════════════════

src/web/public/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,7 @@ <h3>Add Case</h3>
13601360
<div class="modal-tabs">
13611361
<button class="modal-tab-btn active" data-tab="case-create">Create New</button>
13621362
<button class="modal-tab-btn" data-tab="case-link">Link Existing</button>
1363+
<button class="modal-tab-btn" data-tab="case-manage">Manage</button>
13631364
</div>
13641365
<div class="modal-body">
13651366
<!-- Create New Tab -->
@@ -1387,6 +1388,13 @@ <h3>Add Case</h3>
13871388
<span class="form-hint">Absolute path to an existing project folder, e.g. /home/you/my-project</span>
13881389
</div>
13891390
</div>
1391+
<!-- Manage Tab -->
1392+
<div class="modal-tab-content hidden" id="case-manage">
1393+
<div class="case-manage-list" id="caseManageList">
1394+
<!-- Populated by JS -->
1395+
</div>
1396+
<span class="form-hint" style="margin-top: 8px; display: block;">Drag or use arrows to reorder. Changes are saved automatically.</span>
1397+
</div>
13901398
</div>
13911399
<div class="form-actions">
13921400
<button class="btn-toolbar" onclick="app.closeCreateCaseModal()">Cancel</button>

src/web/public/session-ui.js

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,13 +1038,19 @@ Object.assign(CodemanApp.prototype, {
10381038
modal.querySelectorAll('.modal-tab-content').forEach(content => {
10391039
content.classList.toggle('hidden', content.id !== tabName);
10401040
});
1041-
// Update submit button text
1041+
// Update submit button (hide for manage tab)
10421042
const submitBtn = document.getElementById('caseModalSubmit');
1043-
submitBtn.textContent = tabName === 'case-create' ? 'Create' : 'Link';
1043+
if (tabName === 'case-manage') {
1044+
submitBtn.style.display = 'none';
1045+
this.renderCaseManageList();
1046+
} else {
1047+
submitBtn.style.display = '';
1048+
submitBtn.textContent = tabName === 'case-create' ? 'Create' : 'Link';
1049+
}
10441050
// Focus appropriate input
10451051
if (tabName === 'case-create') {
10461052
document.getElementById('newCaseName').focus();
1047-
} else {
1053+
} else if (tabName === 'case-link') {
10481054
document.getElementById('linkCaseName').focus();
10491055
}
10501056
},
@@ -1152,6 +1158,107 @@ Object.assign(CodemanApp.prototype, {
11521158
},
11531159

11541160

1161+
// ═══════════════════════════════════════════════════════════════
1162+
// Case Management (reorder + delete)
1163+
// ═══════════════════════════════════════════════════════════════
1164+
1165+
renderCaseManageList() {
1166+
const container = document.getElementById('caseManageList');
1167+
const cases = this.cases || [];
1168+
if (cases.length === 0) {
1169+
container.innerHTML = '<div class="form-hint" style="text-align: center; padding: 2rem 0;">No cases yet</div>';
1170+
return;
1171+
}
1172+
1173+
let html = '';
1174+
cases.forEach((c, idx) => {
1175+
const isFirst = idx === 0;
1176+
const isLast = idx === cases.length - 1;
1177+
const pathDisplay = c.path ? c.path.replace(/^\/Users\/[^/]+/, '~') : '';
1178+
html += `
1179+
<div class="case-manage-item" data-case="${escapeHtml(c.name)}">
1180+
<div class="case-manage-info">
1181+
<span class="case-manage-name">${escapeHtml(c.name)}</span>
1182+
<span class="case-manage-path">${escapeHtml(pathDisplay)}</span>
1183+
</div>
1184+
<div class="case-manage-actions">
1185+
<button class="case-manage-btn" onclick="app.moveCaseUp('${escapeHtml(c.name)}')"
1186+
title="Move up" ${isFirst ? 'disabled' : ''}>&#x25B2;</button>
1187+
<button class="case-manage-btn" onclick="app.moveCaseDown('${escapeHtml(c.name)}')"
1188+
title="Move down" ${isLast ? 'disabled' : ''}>&#x25BC;</button>
1189+
<button class="case-manage-btn case-manage-btn-delete" onclick="app.deleteCase('${escapeHtml(c.name)}')"
1190+
title="Delete case">&#x2715;</button>
1191+
</div>
1192+
</div>
1193+
`;
1194+
});
1195+
container.innerHTML = html;
1196+
},
1197+
1198+
async moveCaseUp(name) {
1199+
const cases = this.cases || [];
1200+
const idx = cases.findIndex(c => c.name === name);
1201+
if (idx <= 0) return;
1202+
// Swap positions (immutable)
1203+
const reordered = [...cases];
1204+
[reordered[idx - 1], reordered[idx]] = [reordered[idx], reordered[idx - 1]];
1205+
this.cases = reordered;
1206+
this.renderCaseManageList();
1207+
await this.saveCaseOrder(reordered.map(c => c.name));
1208+
},
1209+
1210+
async moveCaseDown(name) {
1211+
const cases = this.cases || [];
1212+
const idx = cases.findIndex(c => c.name === name);
1213+
if (idx < 0 || idx >= cases.length - 1) return;
1214+
const reordered = [...cases];
1215+
[reordered[idx], reordered[idx + 1]] = [reordered[idx + 1], reordered[idx]];
1216+
this.cases = reordered;
1217+
this.renderCaseManageList();
1218+
await this.saveCaseOrder(reordered.map(c => c.name));
1219+
},
1220+
1221+
async deleteCase(name) {
1222+
if (!confirm(`Delete case "${name}"? Linked cases will only be unlinked (folder preserved). Created cases will be permanently deleted.`)) {
1223+
return;
1224+
}
1225+
1226+
try {
1227+
const res = await fetch(`/api/cases/${encodeURIComponent(name)}`, { method: 'DELETE' });
1228+
const data = await res.json();
1229+
if (data.success) {
1230+
this.showToast(`Case "${name}" ${data.data?.type === 'unlinked' ? 'unlinked' : 'deleted'}`, 'success');
1231+
// Remove from current list and refresh
1232+
this.cases = (this.cases || []).filter(c => c.name !== name);
1233+
this.renderCaseManageList();
1234+
// Refresh the dropdown
1235+
const select = document.getElementById('quickStartCase');
1236+
const currentCase = select.value;
1237+
await this.loadQuickStartCases(currentCase === name ? null : currentCase);
1238+
} else {
1239+
this.showToast(data.error || 'Failed to delete case', 'error');
1240+
}
1241+
} catch (err) {
1242+
this.showToast('Failed to delete case: ' + err.message, 'error');
1243+
}
1244+
},
1245+
1246+
async saveCaseOrder(order) {
1247+
try {
1248+
await fetch('/api/cases/order', {
1249+
method: 'PUT',
1250+
headers: { 'Content-Type': 'application/json' },
1251+
body: JSON.stringify({ order })
1252+
});
1253+
// Refresh dropdown to reflect new order
1254+
const select = document.getElementById('quickStartCase');
1255+
const currentCase = select.value;
1256+
await this.loadQuickStartCases(currentCase);
1257+
} catch (err) {
1258+
this.showToast('Failed to save case order: ' + err.message, 'error');
1259+
}
1260+
},
1261+
11551262
// ═══════════════════════════════════════════════════════════════
11561263
// Mobile Case Picker
11571264
// ═══════════════════════════════════════════════════════════════
@@ -1181,6 +1288,11 @@ Object.assign(CodemanApp.prototype, {
11811288
</svg>
11821289
</span>
11831290
<span class="mobile-case-item-name">${escapeHtml(c.name)}</span>
1291+
<span class="mobile-case-item-delete" onclick="event.stopPropagation(); app.deleteCaseMobile('${escapeHtml(c.name)}')" title="Delete">
1292+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1293+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1294+
</svg>
1295+
</span>
11841296
<span class="mobile-case-item-check">
11851297
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
11861298
<polyline points="20 6 9 17 4 12"/>
@@ -1226,6 +1338,25 @@ Object.assign(CodemanApp.prototype, {
12261338
}
12271339
},
12281340

1341+
async deleteCaseMobile(name) {
1342+
if (!confirm(`Delete case "${name}"?`)) return;
1343+
try {
1344+
const res = await fetch(`/api/cases/${encodeURIComponent(name)}`, { method: 'DELETE' });
1345+
const data = await res.json();
1346+
if (data.success) {
1347+
this.showToast(`Case "${name}" ${data.data?.type === 'unlinked' ? 'unlinked' : 'deleted'}`, 'success');
1348+
this.cases = (this.cases || []).filter(c => c.name !== name);
1349+
// Refresh mobile picker and dropdown
1350+
this.closeMobileCasePicker();
1351+
await this.loadQuickStartCases();
1352+
} else {
1353+
this.showToast(data.error || 'Failed to delete case', 'error');
1354+
}
1355+
} catch (err) {
1356+
this.showToast('Failed to delete case: ' + err.message, 'error');
1357+
}
1358+
},
1359+
12291360
showCreateCaseFromMobile() {
12301361
// Close mobile picker first
12311362
this.closeMobileCasePicker();

src/web/public/styles.css

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,6 +2964,93 @@ body {
29642964
font-size: 0.6rem;
29652965
}
29662966

2967+
/* Case Management List (Manage tab) */
2968+
.case-manage-list {
2969+
display: flex;
2970+
flex-direction: column;
2971+
gap: 4px;
2972+
max-height: 320px;
2973+
overflow-y: auto;
2974+
}
2975+
2976+
.case-manage-item {
2977+
display: flex;
2978+
align-items: center;
2979+
justify-content: space-between;
2980+
padding: 8px 10px;
2981+
background: rgba(255, 255, 255, 0.03);
2982+
border: 1px solid rgba(255, 255, 255, 0.06);
2983+
border-radius: 6px;
2984+
transition: background var(--transition-smooth);
2985+
}
2986+
2987+
.case-manage-item:hover {
2988+
background: rgba(255, 255, 255, 0.06);
2989+
}
2990+
2991+
.case-manage-info {
2992+
display: flex;
2993+
flex-direction: column;
2994+
gap: 2px;
2995+
min-width: 0;
2996+
flex: 1;
2997+
}
2998+
2999+
.case-manage-name {
3000+
font-size: 0.8rem;
3001+
font-weight: 500;
3002+
color: var(--text);
3003+
white-space: nowrap;
3004+
overflow: hidden;
3005+
text-overflow: ellipsis;
3006+
}
3007+
3008+
.case-manage-path {
3009+
font-size: 0.65rem;
3010+
color: var(--text-dim);
3011+
white-space: nowrap;
3012+
overflow: hidden;
3013+
text-overflow: ellipsis;
3014+
}
3015+
3016+
.case-manage-actions {
3017+
display: flex;
3018+
gap: 4px;
3019+
margin-left: 10px;
3020+
flex-shrink: 0;
3021+
}
3022+
3023+
.case-manage-btn {
3024+
display: flex;
3025+
align-items: center;
3026+
justify-content: center;
3027+
width: 26px;
3028+
height: 26px;
3029+
background: rgba(255, 255, 255, 0.05);
3030+
border: 1px solid rgba(255, 255, 255, 0.08);
3031+
border-radius: 4px;
3032+
color: var(--text-dim);
3033+
font-size: 0.65rem;
3034+
cursor: pointer;
3035+
transition: all var(--transition-smooth);
3036+
}
3037+
3038+
.case-manage-btn:hover:not(:disabled) {
3039+
background: rgba(255, 255, 255, 0.1);
3040+
color: var(--text);
3041+
}
3042+
3043+
.case-manage-btn:disabled {
3044+
opacity: 0.25;
3045+
cursor: not-allowed;
3046+
}
3047+
3048+
.case-manage-btn-delete:hover:not(:disabled) {
3049+
background: rgba(239, 68, 68, 0.15);
3050+
border-color: rgba(239, 68, 68, 0.3);
3051+
color: #ef4444;
3052+
}
3053+
29673054
.toolbar-input {
29683055
padding: 0.4rem 0.5rem;
29693056
background: var(--bg-input);
@@ -3692,6 +3779,25 @@ body {
36923779
opacity: 1;
36933780
}
36943781

3782+
.mobile-case-item-delete {
3783+
width: 28px;
3784+
height: 28px;
3785+
display: flex;
3786+
align-items: center;
3787+
justify-content: center;
3788+
color: var(--text-dim);
3789+
opacity: 0.4;
3790+
flex-shrink: 0;
3791+
border-radius: 4px;
3792+
transition: all var(--transition-smooth);
3793+
}
3794+
3795+
.mobile-case-item-delete:active {
3796+
opacity: 1;
3797+
color: #ef4444;
3798+
background: rgba(239, 68, 68, 0.15);
3799+
}
3800+
36953801
.mobile-case-picker-footer {
36963802
padding: 12px 20px 16px;
36973803
border-top: 1px solid rgba(255, 255, 255, 0.1);

0 commit comments

Comments
 (0)