From 2069f0fed404a1c240d85f500f52ea13327ee784 Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Wed, 8 Jul 2026 11:59:34 +0300 Subject: [PATCH] Jobs: list / create / edit + attach to changelist - Tools -> Jobs: browse/filter jobs, create/edit job spec - Pending changelist menu -> Attach job... (p4 fix) - Backend p4 jobs, fix, job -o/-i Co-Authored-By: Claude Opus 4.8 --- src-tauri/src/lib.rs | 57 +++++++++++++++++++++++++++++++++++++ src/App.tsx | 67 +++++++++++++++++++++++++++++++++++++++++++- src/i18n.ts | 14 +++++++++ src/p4.ts | 4 +++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cfa8d1d..b0c66e5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1867,6 +1867,59 @@ async fn p4_latest_change(state: State<'_, AppState>, scope: String) -> Result) -> Result, String> { + let conn = current(&state)?; + run_json(&conn, &["jobs", "-m", "300"]) +} + +/// Link a job to a pending changelist (`p4 fix -c `). +#[tauri::command] +async fn p4_fix(state: State<'_, AppState>, job: String, change: String) -> Result { + let conn = current(&state)?; + let job = job.trim(); + let change = change.trim(); + if job.is_empty() || change.is_empty() { + return Err("Job and changelist are required".into()); + } + run_text(&conn, &["fix", "-c", change, job]) +} + +/// Get a job spec as editable text (unknown name → new job template). +#[tauri::command] +async fn p4_job_spec(state: State<'_, AppState>, job: String) -> Result { + let conn = current(&state)?; + let j = job.trim(); + if j.is_empty() { + run_text(&conn, &["job", "-o"]) // template for a new job + } else { + run_text(&conn, &["job", "-o", j]) + } +} + +/// Save a job spec (`p4 job -i`), creating it if new. +#[tauri::command] +async fn p4_job_save(state: State<'_, AppState>, spec: String) -> Result { + let conn = current(&state)?; + let mut child = p4_cmd(&conn, false) + .args(["job", "-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(spec.as_bytes()).map_err(|e| e.to_string())?; + } + let out = child.wait_with_output().map_err(|e| e.to_string())?; + if !out.status.success() { + let err = decode(&out.stderr); + return Err(if err.trim().is_empty() { decode(&out.stdout).trim().to_string() } else { err.trim().to_string() }); + } + Ok(decode(&out.stdout).trim().to_string()) +} + /// List streams on the server. #[tauri::command] async fn p4_streams(state: State<'_, AppState>) -> Result, String> { @@ -2156,6 +2209,10 @@ pub fn run() { p4_streams, p4_switch_stream, p4_stream_integ, + p4_jobs, + p4_fix, + p4_job_spec, + p4_job_save, p4_client_spec, p4_client_save, uasset_thumbnail, diff --git a/src/App.tsx b/src/App.tsx index 7dbe410..9780569 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -313,6 +313,7 @@ type ModalState = | { kind: "labels" } | { kind: "branch" } | { kind: "streams" } + | { kind: "jobs" } | { kind: "client"; name: string; isNew: boolean } | { kind: "blame"; spec: string; name: string } | { kind: "diff"; title: string; text: string } @@ -899,6 +900,13 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set try { await p4.submitChange(change); flash(t("Submitted changelist #{n}.", { n: change })); clearDraftIfCommitted(); await refresh(); await loadHistory(); } // draft clears on successful Submit catch (e) { flash(String(e), true); setBusy(false); } } + function attachJob(change: string) { + setPrompt({ title: t("Attach job to #{n}", { n: change }), label: t("Job ID"), value: "", onSave: async (v) => { + setPrompt(null); const j = v.trim(); if (!j) return; + try { await p4.fix(j, change); flash(t("Job {j} linked to #{n}.", { j, n: change })); } + catch (e) { flash(String(e), true); } + } }); + } async function shelveCL(change: string) { setBusy(true); try { await p4.shelve(change); flash(t("Shelved #{n} on the server.", { n: change })); } @@ -1072,7 +1080,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set ...(uproject ? [{ label: (dockOpen && dockTab === "unreal" ? "✓ " : "") + t("Unreal Log"), icon: I.hex, act: () => openDock("unreal") }] : []), { label: t("File Locks…"), icon: I.lock, act: () => setModal({ kind: "locks" }) }, ], - Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { label: t("Exclusive Locks (typemap)…"), icon: I.lock, act: () => setModal({ kind: "typemap" }) }, { label: t("Labels…"), icon: I.clock, act: () => setModal({ kind: "labels" }) }, { label: t("Integrate / Merge / Copy…"), icon: I.branch, act: () => setModal({ kind: "branch" }) }, { label: t("Streams…"), icon: I.branch, act: () => setModal({ kind: "streams" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, act: startBuild }, { label: t("People & Roles…"), icon: I.people, act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), icon: I.gear, act: () => setModal({ kind: "settings" }) }], + Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { label: t("Exclusive Locks (typemap)…"), icon: I.lock, act: () => setModal({ kind: "typemap" }) }, { label: t("Labels…"), icon: I.clock, act: () => setModal({ kind: "labels" }) }, { label: t("Integrate / Merge / Copy…"), icon: I.branch, act: () => setModal({ kind: "branch" }) }, { label: t("Streams…"), icon: I.branch, act: () => setModal({ kind: "streams" }) }, { label: t("Jobs…"), icon: I.log, act: () => setModal({ kind: "jobs" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, act: startBuild }, { label: t("People & Roles…"), icon: I.people, act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), icon: I.gear, act: () => setModal({ kind: "settings" }) }], Help: [{ label: t("About"), icon: I.info, act: () => setModal({ kind: "about" }) }], }; @@ -1184,6 +1192,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set + + {edit ? (<> +
{t("Edit the job spec. Fields depend on the server's jobspec. Lines starting with # are comments.")}
+