From cd61de548d35ef880e60593a8f016f77f8ab8622 Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Tue, 7 Jul 2026 15:57:51 +0300 Subject: [PATCH] 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 --- src/App.tsx | 34 ++++++++++++++++++++++++++++++---- src/i18n.ts | 6 ++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ad9686f..6a9560e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 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 } ) : ( - + )}
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; checked: Set; 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; checked: Set; onRowClick: (i: number, e: ReactMouseEvent) => void; onToggle: (i: number, dp: string) => void; onContext: (i: number, e: ReactMouseEvent) => void }) { const ref = useRef(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( -
onRowClick(i, e)}> +
onRowClick(i, e)} onContextMenu={(e) => onContext(i, e)}> { e.stopPropagation(); onToggle(i, dp); }}>{I.check} {name}{dir} diff --git a/src/i18n.ts b/src/i18n.ts index 733fd3c..de13292 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -126,6 +126,12 @@ const D: Record = { "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}?" },