Add Pending changelists view (option B)
The Changes tab now shows committed-but-not-submitted changelists as cards above the working set: expandable to their files (click to preview), each with per-CL Submit, plus Edit description / Uncommit (reopen -c default) / Discard in a ⋯ menu, and a Submit-all button. Backend adds p4_reopen_default and p4_set_desc; a small text-prompt modal edits descriptions. Preview panel now shows either the working selection or a clicked pending-CL file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -480,6 +480,69 @@ async fn p4_submit_change(state: State<'_, AppState>, change: String) -> Result<
|
||||
run_stream(&conn, &["submit", "-c", &change], "submit")
|
||||
}
|
||||
|
||||
/// "Uncommit": move files back into the default changelist (like `git reset`).
|
||||
/// The now-empty numbered changelist is cleaned up on the next `p4_changes`.
|
||||
#[tauri::command]
|
||||
async fn p4_reopen_default(state: State<'_, AppState>, files: Vec<String>) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
if files.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let mut args: Vec<&str> = vec!["reopen", "-c", "default"];
|
||||
for f in &files {
|
||||
args.push(f.as_str());
|
||||
}
|
||||
run_text(&conn, &args)
|
||||
}
|
||||
|
||||
/// Edit the description of a pending changelist, preserving its file list.
|
||||
#[tauri::command]
|
||||
async fn p4_set_desc(state: State<'_, AppState>, change: String, description: String) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
let spec = run_text(&conn, &["change", "-o", &change])?;
|
||||
// rebuild the spec, replacing only the Description field's indented body
|
||||
let mut out = String::new();
|
||||
let mut lines = spec.lines().peekable();
|
||||
let mut replaced = false;
|
||||
while let Some(line) = lines.next() {
|
||||
if !replaced && line.starts_with("Description:") {
|
||||
out.push_str("Description:\n");
|
||||
for dl in description.trim().lines() {
|
||||
out.push('\t');
|
||||
out.push_str(dl);
|
||||
out.push('\n');
|
||||
}
|
||||
// consume the old (indented / blank) description body
|
||||
while let Some(p) = lines.peek() {
|
||||
if p.starts_with('\t') || p.starts_with(' ') || p.is_empty() {
|
||||
lines.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
replaced = true;
|
||||
} else {
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
let mut child = p4_cmd(&conn, false)
|
||||
.args(["change", "-i"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
||||
if let Some(si) = child.stdin.as_mut() {
|
||||
si.write_all(out.as_bytes()).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let res = child.wait_with_output().map_err(|e| e.to_string())?;
|
||||
if !res.status.success() {
|
||||
return Err(String::from_utf8_lossy(&res.stderr).trim().to_string());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&res.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// Submit the selected files with a description.
|
||||
#[tauri::command]
|
||||
async fn p4_submit(
|
||||
@ -895,6 +958,8 @@ pub fn run() {
|
||||
p4_submit,
|
||||
p4_commit,
|
||||
p4_submit_change,
|
||||
p4_reopen_default,
|
||||
p4_set_desc,
|
||||
p4_add,
|
||||
p4_edit,
|
||||
p4_delete,
|
||||
|
||||
37
src/App.css
37
src/App.css
@ -122,6 +122,43 @@ body.resizing{cursor:col-resize!important;user-select:none}
|
||||
.pushbar b{color:var(--add)}
|
||||
.pushdot{width:8px;height:8px;border-radius:50%;background:var(--add);box-shadow:0 0 8px var(--add);flex:0 0 auto}
|
||||
.pusharr{margin-left:auto;font-weight:700;font-size:14px}
|
||||
|
||||
/* pending changelists (committed, not yet submitted) */
|
||||
.pending{margin:8px 8px 2px;display:flex;flex-direction:column;gap:6px;max-height:42%;overflow-y:auto;flex:0 0 auto}
|
||||
.pending-h{display:flex;align-items:center;gap:8px;padding:1px 4px 3px}
|
||||
.pending-t{display:flex;align-items:center;gap:8px;font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--add);font-weight:700}
|
||||
.pushall{margin-left:auto;font-size:11.5px;font-weight:600;color:var(--add);background:rgba(62,207,142,.1);border:1px solid rgba(62,207,142,.32);border-radius:8px;padding:4px 11px;cursor:pointer}
|
||||
.pushall:hover{filter:brightness(1.08)}
|
||||
.clcard{border:1px solid rgba(62,207,142,.28);background:rgba(62,207,142,.06);border-radius:11px;overflow:hidden;flex:0 0 auto}
|
||||
.clcard-h{display:flex;align-items:center;gap:9px;padding:9px 11px;cursor:pointer}
|
||||
.clcard-h:hover{background:rgba(62,207,142,.06)}
|
||||
.cl-chev{width:15px;height:15px;flex:0 0 auto;color:var(--faint);display:grid;place-items:center;transition:transform .18s;transform:rotate(-90deg)}
|
||||
.cl-chev.open{transform:rotate(0deg)}
|
||||
.cl-chev svg{width:15px;height:15px}
|
||||
.cl-num{font-family:var(--mono);font-size:12px;color:var(--accent-2);flex:0 0 auto;font-variant-numeric:tabular-nums}
|
||||
.cl-desc{flex:1;min-width:0;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cl-count{flex:0 0 auto;font-size:11px;color:var(--faint)}
|
||||
.cl-actions{flex:0 0 auto;display:flex;align-items:center;gap:5px}
|
||||
.cl-submit{width:27px;height:27px;border:none;border-radius:8px;cursor:pointer;color:#fff;font-size:14px;font-weight:700;
|
||||
background:linear-gradient(135deg,var(--add),#1f9f6e);box-shadow:0 4px 12px -5px rgba(62,207,142,.7)}
|
||||
.cl-submit:hover{filter:brightness(1.1)}
|
||||
.cl-more{width:27px;height:27px;border:1px solid var(--border);background:var(--panel);color:var(--muted);border-radius:8px;cursor:pointer;font-size:15px;line-height:1;display:grid;place-items:center}
|
||||
.cl-more:hover{color:var(--txt);border-color:var(--accent)}
|
||||
.cl-files{border-top:1px solid rgba(62,207,142,.2);padding:4px}
|
||||
.cl-empty{padding:8px 10px;font-size:11.5px;color:var(--faint)}
|
||||
.cl-file{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:8px;cursor:pointer}
|
||||
.cl-file:hover{background:var(--hover)}
|
||||
.cl-file.sel{background:rgba(124,110,246,.14);box-shadow:inset 0 0 0 1px rgba(124,110,246,.25)}
|
||||
.cl-fname{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
||||
.cl-fname .n{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cl-fname .p{font-size:10.5px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* text prompt (edit description) */
|
||||
.modal-card.prompt{width:460px;max-width:calc(100vw - 40px);text-align:left;align-items:stretch}
|
||||
.modal-card.prompt h3{text-align:left}
|
||||
.prompt-label{font-size:11.5px;color:var(--faint);margin:2px 0 9px}
|
||||
.prompt-input{width:100%;min-height:96px;resize:vertical;background:var(--input-bg);border:1px solid var(--border);border-radius:10px;padding:10px 12px;font-size:13px;color:var(--txt);font-family:var(--font);outline:none;line-height:1.5}
|
||||
.prompt-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,110,246,.15)}
|
||||
.chhead{display:flex;align-items:center;gap:10px;padding:9px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border-soft)}
|
||||
.chk{width:16px;height:16px;border-radius:6px;border:1.5px solid var(--accent);
|
||||
background:linear-gradient(145deg,var(--accent-2),var(--accent-deep));display:grid;place-items:center;flex:0 0 auto;box-shadow:0 0 8px rgba(124,110,246,.4)}
|
||||
|
||||
119
src/App.tsx
119
src/App.tsx
@ -276,6 +276,9 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
const [checked, setChecked] = useState<Set<string>>(new Set());
|
||||
const [sel, setSel] = useState<number | null>(null); // primary row (preview + shift anchor)
|
||||
const [selRows, setSelRows] = useState<Set<number>>(new Set()); // multi-selection (Shift/Ctrl-click)
|
||||
const [previewCL, setPreviewCL] = useState<OpenedFile | null>(null); // preview override (pending-CL file)
|
||||
const [expandedCL, setExpandedCL] = useState<Set<string>>(new Set()); // expanded pending changelist cards
|
||||
const [prompt, setPrompt] = useState<null | { title: string; label: string; value: string; onSave: (v: string) => void }>(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [desc, setDesc] = useState(""); // commit summary (first line)
|
||||
const [descBody, setDescBody] = useState(""); // extended description (optional)
|
||||
@ -340,6 +343,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
setChecked(new Set(uncommitted.map((x) => x.depotFile || "").filter(Boolean)));
|
||||
setSel((prev) => (prev != null && prev < f.length ? prev : f.length ? 0 : null));
|
||||
setSelRows(new Set());
|
||||
setPreviewCL(null);
|
||||
try { setPending(await p4.changes()); } catch { setPending([]); }
|
||||
}
|
||||
|
||||
@ -596,6 +600,38 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
await loadHistory(); // the submit created a new submitted changelist — refresh History
|
||||
}
|
||||
|
||||
// Submit a single pending changelist to the server
|
||||
async function submitOne(change: string) {
|
||||
if (!(await confirm({ title: t("Submit changelist #{n}?", { n: change }), body: t("This changelist goes to the Perforce server and becomes available to the team. This is a push — it cannot be undone."), confirm: t("Submit") }))) return;
|
||||
setBusy(true);
|
||||
try { await p4.submitChange(change); flash(t("Submitted changelist #{n}.", { n: change })); await refresh(); await loadHistory(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
}
|
||||
// Uncommit: move a changelist's files back into the working set (default)
|
||||
async function uncommit(fs: OpenedFile[]) {
|
||||
const dps = fs.map((f) => f.depotFile || "").filter(Boolean);
|
||||
if (!dps.length) return;
|
||||
setBusy(true);
|
||||
try { await p4.reopenDefault(dps); flash(t("Moved back to working changes.")); await refresh(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
}
|
||||
// Edit a pending changelist's description
|
||||
function editDesc(cl: Change) {
|
||||
setPrompt({
|
||||
title: t("Edit description"), label: t("Changelist #{n}", { n: cl.change || "" }), value: (cl.desc || "").trim(),
|
||||
onSave: async (v) => {
|
||||
setPrompt(null);
|
||||
if (!v.trim()) return;
|
||||
setBusy(true);
|
||||
try { await p4.setDesc(cl.change || "", v.trim()); flash(t("Description updated.")); await refresh(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
},
|
||||
});
|
||||
}
|
||||
function toggleCL(change: string) {
|
||||
setExpandedCL((prev) => { const n = new Set(prev); n.has(change) ? n.delete(change) : n.add(change); return n; });
|
||||
}
|
||||
|
||||
async function doRevert(file: OpenedFile) {
|
||||
const dp = file.depotFile || "";
|
||||
if (!(await confirm({ title: t("Revert file?"), body: t("{name}\n\nLocal edits to this file will be lost.", { name: splitPath(dp).name }), confirm: t("Revert"), danger: true }))) return;
|
||||
@ -610,7 +646,13 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
const q = filter.toLowerCase();
|
||||
return uncommitted.filter((f) => (f.depotFile || "").toLowerCase().includes(q));
|
||||
}, [uncommitted, filter]);
|
||||
const selFile = sel != null ? view[sel] : undefined;
|
||||
// committed pending changelists with their files (grouped from p4 opened)
|
||||
const pendingCards = useMemo(() =>
|
||||
pending.map((cl) => ({
|
||||
cl,
|
||||
files: files.filter((f) => String(f.change || "") === String(cl.change || "")),
|
||||
})), [pending, files]);
|
||||
const selFile = previewCL ?? (sel != null ? view[sel] : undefined);
|
||||
const allChecked = view.length > 0 && view.every((f) => checked.has(f.depotFile || ""));
|
||||
function toggleAll() {
|
||||
setChecked((s) => {
|
||||
@ -633,6 +675,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
setSelRows(new Set([i]));
|
||||
}
|
||||
setSel(i);
|
||||
setPreviewCL(null); // show the working-set selection in the preview
|
||||
}
|
||||
// checkbox: if the row is part of a multi-selection, toggle the whole selection together
|
||||
function toggleCheck(i: number, dp: string) {
|
||||
@ -752,11 +795,49 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
<div className="filter">
|
||||
<div className="box">{I.search}<input placeholder={t("Filter {n} files…", { n: uncommitted.length })} value={filter} onChange={(e) => { setFilter(e.target.value); setSelRows(new Set()); }} /></div>
|
||||
</div>
|
||||
{pending.length > 0 && (
|
||||
<div className="pushbar" onClick={pushAll} title={t("Submit to server")}>
|
||||
<span className="pushdot" />
|
||||
<span>{t("{n} changelist(s) committed — ready to Submit", { n: pending.length })}</span>
|
||||
<span className="pusharr">↑</span>
|
||||
{pendingCards.length > 0 && (
|
||||
<div className="pending">
|
||||
<div className="pending-h">
|
||||
<span className="pending-t"><span className="pushdot" />{t("Pending · ready to Submit")}</span>
|
||||
<button className="pushall" onClick={pushAll} title={t("Submit all to the server")}>{t("Submit all")} ↑</button>
|
||||
</div>
|
||||
{pendingCards.map(({ cl, files: cf }) => {
|
||||
const ch = cl.change || "";
|
||||
const open = expandedCL.has(ch);
|
||||
return (
|
||||
<div key={ch} className="clcard">
|
||||
<div className="clcard-h" onClick={() => toggleCL(ch)}>
|
||||
<span className={"cl-chev" + (open ? " open" : "")}>{I.chev}</span>
|
||||
<span className="cl-num">#{ch}</span>
|
||||
<span className="cl-desc">{(cl.desc || "").trim() || t("(no description)")}</span>
|
||||
<span className="cl-count">{t("{n} files", { n: cf.length })}</span>
|
||||
<span className="cl-actions" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="cl-submit" onClick={() => submitOne(ch)} title={t("Submit to server")}>↑</button>
|
||||
<button className="cl-more" onClick={(e) => openCtx(e, [
|
||||
{ label: t("Edit description"), act: () => editDesc(cl) },
|
||||
{ label: t("Uncommit (back to working)"), act: () => uncommit(cf) },
|
||||
{ label: t("Discard changes · {n} files", { n: cf.length }), danger: true, act: () => discard(cf) },
|
||||
])} title={t("More")}>⋯</button>
|
||||
</span>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="cl-files">
|
||||
{cf.length === 0 && <div className="cl-empty">{t("(files are outside the current working folder)")}</div>}
|
||||
{cf.map((f) => {
|
||||
const sp = splitPath(f.depotFile);
|
||||
const st = statusOf(f.action);
|
||||
return (
|
||||
<div key={f.depotFile} className={"cl-file" + (previewCL?.depotFile === f.depotFile ? " sel" : "")} onClick={() => setPreviewCL(f)}>
|
||||
<span className={"stat " + st.cls}>{st.ch}</span>
|
||||
<span className="cl-fname"><span className="n">{sp.name}</span><span className="p">{sp.dir}</span></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{scanning && (
|
||||
@ -837,6 +918,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{modal && <AppModal modal={modal} info={info} light={light} toggleTheme={toggleTheme} lang={lang} setLang={setLang} zoom={zoom} setZoom={setZoom}
|
||||
onClose={() => setModal(null)} onPickWorkspace={switchWorkspace} onBrowse={browseTo} onChoose={chooseScope} onDisconnect={onDisconnect} />}
|
||||
{ask && <ConfirmModal {...ask} onClose={(v) => { ask.resolve(v); setAsk(null); }} />}
|
||||
{prompt && <TextPrompt title={prompt.title} label={prompt.label} value={prompt.value} onSave={prompt.onSave} onClose={() => setPrompt(null)} />}
|
||||
{showXfer && transfer && <TransferDialog transfer={transfer} />}
|
||||
{buildOpen && !buildMin && <BuildOverlay lines={buildLog} building={building} ok={buildOk} sln={slnPath} onMinimize={() => setBuildMin(true)} onClose={() => setBuildOpen(false)} />}
|
||||
{buildOpen && buildMin && <BuildChip building={building} ok={buildOk} onExpand={() => setBuildMin(false)} onClose={() => setBuildOpen(false)} />}
|
||||
@ -1082,6 +1164,31 @@ function BuildChip({ building, ok, onExpand, onClose }: { building: boolean; ok:
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- text prompt (edit description) ---------------- */
|
||||
function TextPrompt({ title, label, value, onSave, onClose }: { title: string; label: string; value: string; onSave: (v: string) => void; onClose: () => void }) {
|
||||
const [v, setV] = useState(value);
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
ref.current?.focus();
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) onSave(v); };
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [v]); // eslint-disable-line
|
||||
return (
|
||||
<div className="modal-back" onClick={onClose}>
|
||||
<div className="modal-card prompt" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>{title}</h3>
|
||||
<div className="prompt-label">{label}</div>
|
||||
<textarea ref={ref} className="prompt-input" value={v} onChange={(e) => setV(e.target.value)} />
|
||||
<div className="modal-actions">
|
||||
<button className="mbtn ghost" onClick={onClose}>{t("Cancel")}</button>
|
||||
<button className="mbtn primary" onClick={() => onSave(v)}>{t("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- confirm modal ---------------- */
|
||||
function ConfirmModal({ title, body, confirm, danger, onClose }: { title: string; body: string; confirm: string; danger?: boolean; onClose: (v: boolean) => void }) {
|
||||
useEffect(() => {
|
||||
|
||||
15
src/i18n.ts
15
src/i18n.ts
@ -86,6 +86,21 @@ const D: Record<string, Tr> = {
|
||||
"History": { ru: "History", de: "Verlauf", fr: "Historique", es: "Historial" },
|
||||
"Filter {n} files…": { ru: "Фильтр {n} файлов…", de: "{n} Dateien filtern…", fr: "Filtrer {n} fichiers…", es: "Filtrar {n} archivos…" },
|
||||
"{n} changelist(s) committed — ready to Submit": { ru: "{n} changelist(ов) закоммичено — готово к Submit", de: "{n} Changelist(s) committed — bereit zum Submit", fr: "{n} changelist(s) validés — prêts à envoyer", es: "{n} changelist(s) confirmados — listos para enviar" },
|
||||
"Pending · ready to Submit": { ru: "Pending · готово к Submit", de: "Ausstehend · bereit zum Submit", fr: "En attente · prêt à envoyer", es: "Pendiente · listo para enviar" },
|
||||
"Submit all": { ru: "Отправить всё", de: "Alle senden", fr: "Tout envoyer", es: "Enviar todo" },
|
||||
"Submit all to the server": { ru: "Отправить все на сервер", de: "Alle an Server senden", fr: "Tout envoyer au serveur", es: "Enviar todo al servidor" },
|
||||
"Submit": { ru: "Отправить", de: "Senden", fr: "Envoyer", es: "Enviar" },
|
||||
"More": { ru: "Ещё", de: "Mehr", fr: "Plus", es: "Más" },
|
||||
"Submit changelist #{n}?": { ru: "Отправить changelist #{n}?", de: "Changelist #{n} senden?", fr: "Envoyer le changelist #{n} ?", es: "¿Enviar changelist #{n}?" },
|
||||
"This changelist goes to the Perforce server and becomes available to the team. This is a push — it cannot be undone.": { ru: "Этот changelist уйдёт в депо Perforce и станет доступен команде. Это push — отменить нельзя.", de: "Diese Changelist geht an den Perforce-Server und wird für das Team verfügbar. Das ist ein Push — nicht widerrufbar.", fr: "Ce changelist ira sur le serveur Perforce et sera disponible pour l’équipe. C’est un push — irréversible.", es: "Este changelist irá al servidor Perforce y estará disponible para el equipo. Es un push — no se puede deshacer." },
|
||||
"Submitted changelist #{n}.": { ru: "Changelist #{n} отправлен.", de: "Changelist #{n} übermittelt.", fr: "Changelist #{n} envoyé.", es: "Changelist #{n} enviado." },
|
||||
"Uncommit (back to working)": { ru: "Разкоммитить (обратно в Working)", de: "Uncommit (zurück zu Working)", fr: "Décommiter (retour au travail)", es: "Descomitear (volver a trabajo)" },
|
||||
"Moved back to working changes.": { ru: "Возвращено в текущие изменения.", de: "Zurück zu den Arbeitsänderungen verschoben.", fr: "Renvoyé aux modifications en cours.", es: "Devuelto a los cambios de trabajo." },
|
||||
"Edit description": { ru: "Изменить описание", de: "Beschreibung bearbeiten", fr: "Modifier la description", es: "Editar descripción" },
|
||||
"Changelist #{n}": { ru: "Changelist #{n}", de: "Changelist #{n}", fr: "Changelist #{n}", es: "Changelist #{n}" },
|
||||
"Description updated.": { ru: "Описание обновлено.", de: "Beschreibung aktualisiert.", fr: "Description mise à jour.", es: "Descripción actualizada." },
|
||||
"(files are outside the current working folder)": { ru: "(файлы вне текущей рабочей папки)", de: "(Dateien außerhalb des Arbeitsordners)", fr: "(fichiers hors du dossier de travail)", es: "(archivos fuera de la carpeta de trabajo)" },
|
||||
"Save": { ru: "Сохранить", de: "Speichern", fr: "Enregistrer", es: "Guardar" },
|
||||
"Scanning disk for changes…": { ru: "Ищу изменения на диске…", de: "Suche Änderungen auf der Festplatte…", fr: "Recherche de modifications sur le disque…", es: "Buscando cambios en el disco…" },
|
||||
" for the whole workspace this is slow — pick a project folder above": { ru: " для всего workspace это долго — выбери рабочую папку проекта сверху", de: " für den ganzen Workspace ist das langsam — wähle oben einen Projektordner", fr: " pour tout le workspace c’est lent — choisissez un dossier de projet en haut", es: " para todo el workspace es lento — elige una carpeta de proyecto arriba" },
|
||||
"{a} of {b} selected": { ru: "{a} из {b} выбрано", de: "{a} von {b} ausgewählt", fr: "{a} sur {b} sélectionnés", es: "{a} de {b} seleccionados" },
|
||||
|
||||
@ -64,6 +64,8 @@ export const p4 = {
|
||||
commit: (description: string, files: string[]) =>
|
||||
invoke<string>("p4_commit", { description, files }),
|
||||
submitChange: (change: string) => invoke<string>("p4_submit_change", { change }),
|
||||
reopenDefault: (files: string[]) => invoke<string>("p4_reopen_default", { files }),
|
||||
setDesc: (change: string, description: string) => invoke<string>("p4_set_desc", { change, description }),
|
||||
add: (files: string[]) => invoke<OpenedFile[]>("p4_add", { files }),
|
||||
edit: (files: string[]) => invoke<OpenedFile[]>("p4_edit", { files }),
|
||||
del: (files: string[]) => invoke<OpenedFile[]>("p4_delete", { files }),
|
||||
|
||||
Reference in New Issue
Block a user