Add Discard changes (revert) via right-click on Changes rows

Right-clicking a file in Changes opens a menu with "Discard changes"
(p4 revert, confirmed, danger) plus Edit in VS Code / Open in Explorer /
Copy path. Operates on the whole multi-selection when the row is part of
it; right-clicking an unselected row selects it first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-07 15:57:51 +03:00
parent 670acf5242
commit cd61de548d
2 changed files with 36 additions and 4 deletions

View File

@ -616,6 +616,32 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
setChecked((prev) => { const n = new Set(prev); dps.forEach((d) => (target ? n.add(d) : n.delete(d))); return n; });
}
// Discard local changes (p4 revert) for the given files — the "undo" for opened files.
async function discard(fs: OpenedFile[]) {
const dps = fs.map((f) => f.depotFile || "").filter(Boolean);
if (!dps.length) return;
const label = dps.length === 1 ? splitPath(dps[0]).name : t("{n} files", { n: dps.length });
if (!(await confirm({ title: t("Discard changes?"), body: t("Local changes to {name} will be lost permanently — the file returns to the server version.", { name: label }), confirm: t("Discard"), danger: true }))) return;
setBusy(true);
try { await p4.revert(dps); flash(t("Discarded: {name}", { name: label })); await refresh(); }
catch (e) { flash(String(e), true); setBusy(false); }
}
// right-click on a Changes row → file actions (operates on the selection if the row is in it)
function fileCtx(i: number, e: ReactMouseEvent) {
const multi = selRows.has(i) && selRows.size > 1;
if (!selRows.has(i)) { setSelRows(new Set([i])); setSel(i); } // right-click selects the row
const idxs = multi ? [...selRows] : [i];
const targets = idxs.map((k) => view[k]).filter(Boolean) as OpenedFile[];
const dp = view[i]?.depotFile || "";
const isCode = kindOf(splitPath(dp).name) === "code";
openCtx(e, [
{ label: targets.length > 1 ? t("Discard changes · {n} files", { n: targets.length }) : t("Discard changes"), danger: true, act: () => discard(targets) },
...(isCode ? [{ label: t("Edit in VS Code"), icon: I.vscode, act: () => { p4.openInVscode(dp).then(() => flash(t("Opening in VS Code…"))).catch((err) => flash(String(err), true)); } }] : []),
{ label: t("Open in Explorer"), act: () => reveal(dp) },
{ label: t("Copy path"), act: () => copyText(dp, t("Path")) },
]);
}
const menus: Record<string, { label: string; kb?: string; ext?: boolean; act?: () => void }[]> = {
File: [{ label: t("Switch workspace…"), act: openWorkspaces }, { label: t("Choose working folder…"), act: () => browseTo("") }, { label: t("Disconnect"), act: onDisconnect }],
Connection: [{ label: t("Refresh"), kb: "F5", act: () => refresh() }, { label: t("Choose working folder…"), act: () => browseTo("") }],
@ -727,7 +753,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
</>}
</div>
) : (
<FileList files={view} sel={sel} selRows={selRows} checked={checked} onRowClick={rowClick} onToggle={toggleCheck} />
<FileList files={view} sel={sel} selRows={selRows} checked={checked} onRowClick={rowClick} onToggle={toggleCheck} onContext={fileCtx} />
)}
<div className="commit">
<input className="summary" placeholder={t("Summary: what changed…")} value={desc} onChange={(e) => setDesc(e.target.value)}
@ -1107,8 +1133,8 @@ function Thumb({ file }: { file: OpenedFile }) {
/* ---------------- virtualized file list ---------------- */
const ROW = 52;
function FileList({ files, sel, selRows, checked, onRowClick, onToggle }:
{ files: OpenedFile[]; sel: number | null; selRows: Set<number>; checked: Set<string>; onRowClick: (i: number, e: ReactMouseEvent) => void; onToggle: (i: number, dp: string) => void }) {
function FileList({ files, sel, selRows, checked, onRowClick, onToggle, onContext }:
{ files: OpenedFile[]; sel: number | null; selRows: Set<number>; checked: Set<string>; onRowClick: (i: number, e: ReactMouseEvent) => void; onToggle: (i: number, dp: string) => void; onContext: (i: number, e: ReactMouseEvent) => void }) {
const ref = useRef<HTMLDivElement>(null);
const [scroll, setScroll] = useState(0);
const [h, setH] = useState(500);
@ -1130,7 +1156,7 @@ function FileList({ files, sel, selRows, checked, onRowClick, onToggle }:
const st = statusOf(f.action);
const isCk = checked.has(dp);
rows.push(
<div key={dp + i} className={"file" + (selRows.has(i) ? " sel" : "") + (i === sel ? " primary" : "")} style={{ top: i * ROW }} onClick={(e) => onRowClick(i, e)}>
<div key={dp + i} className={"file" + (selRows.has(i) ? " sel" : "") + (i === sel ? " primary" : "")} style={{ top: i * ROW }} onClick={(e) => onRowClick(i, e)} onContextMenu={(e) => onContext(i, e)}>
<span className={"fchk " + (isCk ? "ck" : "off")} onClick={(e) => { e.stopPropagation(); onToggle(i, dp); }}>{I.check}</span>
<Thumb file={f} />
<span className="fname"><span className="n">{name}</span><span className="p">{dir}</span></span>

View File

@ -126,6 +126,12 @@ const D: Record<string, Tr> = {
"Revert file?": { ru: "Откатить файл?", de: "Datei zurücksetzen?", fr: "Annuler le fichier ?", es: "¿Revertir archivo?" },
"{name}\n\nLocal edits to this file will be lost.": { ru: "{name}\n\nЛокальные правки этого файла будут потеряны.", de: "{name}\n\nLokale Änderungen an dieser Datei gehen verloren.", fr: "{name}\n\nLes modifications locales de ce fichier seront perdues.", es: "{name}\n\nLos cambios locales de este archivo se perderán." },
"Revert": { ru: "Откатить", de: "Zurücksetzen", fr: "Annuler", es: "Revertir" },
"Discard changes": { ru: "Отменить изменения", de: "Änderungen verwerfen", fr: "Abandonner les modifications", es: "Descartar cambios" },
"Discard changes · {n} files": { ru: "Отменить изменения · {n} файлов", de: "Änderungen verwerfen · {n} Dateien", fr: "Abandonner · {n} fichiers", es: "Descartar · {n} archivos" },
"Discard changes?": { ru: "Отменить изменения?", de: "Änderungen verwerfen?", fr: "Abandonner les modifications ?", es: "¿Descartar cambios?" },
"Discard": { ru: "Отменить", de: "Verwerfen", fr: "Abandonner", es: "Descartar" },
"Local changes to {name} will be lost permanently — the file returns to the server version.": { ru: "Локальные изменения в {name} будут потеряны безвозвратно — файл вернётся к версии с сервера.", de: "Lokale Änderungen an {name} gehen endgültig verloren — die Datei kehrt zur Serverversion zurück.", fr: "Les modifications locales de {name} seront perdues définitivement — le fichier revient à la version du serveur.", es: "Los cambios locales en {name} se perderán para siempre — el archivo vuelve a la versión del servidor." },
"Discarded: {name}": { ru: "Отменено: {name}", de: "Verworfen: {name}", fr: "Abandonné : {name}", es: "Descartado: {name}" },
"Edit in VS Code": { ru: "Открыть в VS Code", de: "In VS Code öffnen", fr: "Ouvrir dans VS Code", es: "Abrir en VS Code" },
"Opening in VS Code…": { ru: "Открываю в VS Code…", de: "Wird in VS Code geöffnet…", fr: "Ouverture dans VS Code…", es: "Abriendo en VS Code…" },
"Revert changelist #{n}?": { ru: "Откатить changelist #{n}?", de: "Changelist #{n} zurücksetzen?", fr: "Annuler le changelist #{n} ?", es: "¿Revertir changelist #{n}?" },