feat(cases): rename in the app's own dialog instead of browser prompts

This commit is contained in:
Devvyn committed 2026-10-23 04:12:37 +08:00
1 parent ab0bc0aecd
commit 201603e57b
3 files changed
+237 -73

No files matched your search

+29
View File
@@ -3792,6 +3792,35 @@
</div>
</div>
<!-- Rename a case (and its folder): opened by the Manage tab's pencil button, see renameCase() in session-ui.js.
Placed after the case modal so it stacks above it. -->
<div class="modal" id="renameCaseModal">
<div class="modal-backdrop" onclick="app.closeRenameCase()"></div>
<div class="modal-content modal-sm">
<div class="modal-header">
<h3>Rename case</h3>
<button class="modal-close" onclick="app.closeRenameCase()" aria-label="Cancel">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<label for="renameCaseInput" id="renameCaseLabel">New name</label>
<input type="text" id="renameCaseInput" class="set-input" maxlength="100" autocomplete="off" spellcheck="false"
placeholder="letters, numbers, - and _" onkeydown="if (event.key === 'Enter') app.submitRenameCase()">
</div>
<div class="form-row hidden" id="renameCaseFolderRow">
<label class="checkbox-row"><input type="checkbox" id="renameCaseFolder"> Also rename the folder on disk</label>
<span class="form-hint" id="renameCaseFolderHint" data-i18n-skip></span>
</div>
<p class="form-hint" id="renameCaseNote">A case under the cases folder is renamed together with its folder.</p>
<p class="form-hint hidden" id="renameCaseError" role="alert" style="color: var(--danger, #e5534b);"></p>
</div>
<div class="modal-footer">
<button class="btn-toolbar" onclick="app.closeRenameCase()">Cancel</button>
<button class="btn-toolbar btn-primary" id="renameCaseSubmit" onclick="app.submitRenameCase()">Rename</button>
</div>
</div>
</div>
<!-- Mobile Case Picker (Bottom Sheet) -->
<div class="modal mobile-case-picker" id="mobileCasePickerModal">
<div class="modal-backdrop" onclick="app.closeMobileCasePicker()"></div>
+70 -29
View File
@@ -4996,48 +4996,86 @@ Object.assign(CodemanApp.prototype, {
},
/**
* Rename a case and its folder (POST /api/cases/:name/rename). A linked case asks whether the real folder
* should be renamed too (Cancel renames only the alias). When live sessions have the folder open the server
* refuses and lists them; they are only stopped after a second, explicit confirmation.
* Rename a case and its folder (POST /api/cases/:name/rename), in the app's own dialog.
*
* A linked case shows a checkbox for renaming the real folder too (unticked = the name only). When live
* sessions have the folder open the server refuses and lists them; the dialog then lists them and its button
* becomes "Stop sessions and rename", so they are only stopped after a second, explicit press. Errors from
* the server (a taken name, a folder another program has open) stay inside the dialog so the name can be fixed.
*/
async renameCase(name) {
renameCase(name) {
const info = (this.cases || []).find((c) => c.name === name);
const entered = prompt(`Rename case "${name}" to (letters, numbers, - and _):`, name);
const newName = (entered || '').trim();
if (!newName || newName === name) return;
this._renameCaseState = { name, linked: Boolean(info?.linked), stop: false, busy: false };
const input = document.getElementById('renameCaseInput');
const label = document.getElementById('renameCaseLabel');
if (label) label.textContent = `New name for "${name}"`;
if (input) input.value = name;
const row = document.getElementById('renameCaseFolderRow');
row?.classList.toggle('hidden', !info?.linked);
const box = document.getElementById('renameCaseFolder');
if (box) box.checked = false;
const hint = document.getElementById('renameCaseFolderHint');
if (hint) hint.textContent = info?.linked ? `${info.path} (unticked: only the name in this list changes)` : '';
document.getElementById('renameCaseNote')?.classList.toggle('hidden', Boolean(info?.linked));
this._renameCaseNotice('');
const submit = document.getElementById('renameCaseSubmit');
if (submit) submit.textContent = 'Rename';
document.getElementById('renameCaseModal')?.classList.add('active');
input?.focus?.();
input?.select?.();
},
closeRenameCase() {
document.getElementById('renameCaseModal')?.classList.remove('active');
this._renameCaseState = null;
},
/** Show (or clear, with '') a message inside the rename dialog. */
_renameCaseNotice(message) {
const el = document.getElementById('renameCaseError');
if (!el) return;
el.textContent = message;
el.classList.toggle('hidden', !message);
},
async submitRenameCase() {
const state = this._renameCaseState;
if (!state || state.busy) return;
const name = state.name;
const newName = (document.getElementById('renameCaseInput')?.value || '').trim();
if (!newName || newName === name) {
this._renameCaseNotice('Enter a different name.');
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(newName)) {
this.showToast('Use only letters, numbers, hyphens and underscores', 'error');
this._renameCaseNotice('Use only letters, numbers, hyphens and underscores.');
return;
}
const body = { newName };
if (info?.linked) {
body.renameFolder = confirm(
`Also rename the folder on disk?\n\n${info.path}\n\nOK renames the folder to "${newName}" next to it. ` +
`Cancel keeps the folder and only changes the case name.`
);
}
const send = async () => {
if (state.linked) body.renameFolder = Boolean(document.getElementById('renameCaseFolder')?.checked);
if (state.stop) body.stopSessions = true;
state.busy = true;
const submit = document.getElementById('renameCaseSubmit');
if (submit) submit.disabled = true;
try {
const res = await fetch(`/api/cases/${encodeURIComponent(name)}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return res.json().catch(() => ({ success: false, error: 'Unexpected response' }));
};
try {
let data = await send();
const data = await res.json().catch(() => ({ success: false, error: 'Unexpected response' }));
if (data.requiresStop) {
const list = (data.sessions || []).map((s) => ` - ${s.name}`).join('\n');
if (!confirm(`These sessions have the folder open and will be stopped (nothing is restarted):\n\n${list}\n\nStop them and rename?`)) {
return;
}
body.stopSessions = true;
data = await send();
}
if (!data.success) {
this.showToast(data.error || 'Failed to rename case', 'error');
const list = (data.sessions || []).map((s) => `${s.name}`).join(', ');
state.stop = true;
this._renameCaseNotice(`These sessions have the folder open: ${list}. They will be stopped (nothing is restarted) before the folder is renamed.`);
if (submit) submit.textContent = 'Stop sessions and rename';
return;
}
if (!data.success) {
this._renameCaseNotice(data.error || 'Failed to rename the case.');
return;
}
this.closeRenameCase();
this.showToast(`Case renamed to "${newName}"`, 'success');
// The per-case run options are stored under the case name in this browser.
try {
@@ -5052,7 +5090,10 @@ Object.assign(CodemanApp.prototype, {
await this.loadQuickStartCases(current === name ? newName : current);
this.renderCaseManageList();
} catch (err) {
this.showToast('Failed to rename case: ' + err.message, 'error');
this._renameCaseNotice('Failed to rename the case: ' + err.message);
} finally {
state.busy = false;
if (submit) submit.disabled = false;
}
},