Streams: list / switch / merge down / copy up
- Tools -> Streams: list server streams, switch the workspace to a stream, merge down from parent / copy up to parent (into a pending changelist) - Backend p4 streams, client -s -S, merge/copy -S Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
62
src/App.tsx
62
src/App.tsx
@ -312,6 +312,7 @@ type ModalState =
|
||||
| { kind: "typemap" }
|
||||
| { kind: "labels" }
|
||||
| { kind: "branch" }
|
||||
| { kind: "streams" }
|
||||
| { kind: "client"; name: string; isNew: boolean }
|
||||
| { kind: "blame"; spec: string; name: string }
|
||||
| { kind: "diff"; title: string; text: string }
|
||||
@ -733,6 +734,16 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
try { setModal({ kind: "scope", path, dirs: await p4.dirs(path) }); }
|
||||
catch (e) { flash(String(e), true); }
|
||||
}
|
||||
async function doSwitchStream(stream: string) {
|
||||
setModal(null); setBusy(true);
|
||||
try { const i = await p4.switchStream(stream); onInfo(i); flash(t("Switched to stream {s}", { s: stream })); await refresh(); await loadHistory(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
}
|
||||
async function doStreamInteg(stream: string, dir: "down" | "up") {
|
||||
setBusy(true);
|
||||
try { const out = await p4.streamInteg(stream, dir); flash(t("{d} opened into a pending changelist — resolve & submit. {out}", { d: dir === "up" ? t("Copy up") : t("Merge down"), out: out.split("\n")[0] })); await refresh(); }
|
||||
catch (e) { flash(String(e), true); } finally { setBusy(false); }
|
||||
}
|
||||
function chooseScope(path: string) {
|
||||
setModal(null);
|
||||
setActivePath(path);
|
||||
@ -1061,7 +1072,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: 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: 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" }) }],
|
||||
};
|
||||
|
||||
@ -1317,6 +1328,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{modal?.kind === "typemap" && <TypemapModal onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "labels" && <LabelsModal scope={activePath} onClose={() => setModal(null)} onFlash={flash} onSyncTo={(l) => { setModal(null); syncTo(l); }} />}
|
||||
{modal?.kind === "branch" && <BranchModal scope={activePath} onClose={() => setModal(null)} onFlash={flash} onDone={() => { setModal(null); refresh(); }} />}
|
||||
{modal?.kind === "streams" && <StreamsModal current={String(info?.Stream || "")} onClose={() => setModal(null)} onFlash={flash} onSwitch={doSwitchStream} onInteg={doStreamInteg} />}
|
||||
{modal?.kind === "client" && <ClientSpecModal name={modal.name} isNew={modal.isNew} onClose={() => setModal(null)} onFlash={flash} onSaved={(c) => { setModal(null); if (modal.isNew) { switchWorkspace(c); } else { refresh(); } }} />}
|
||||
{modal?.kind === "blame" && <BlameModal spec={modal.spec} name={modal.name} onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "diff" && <DiffModal title={modal.title} text={modal.text} onClose={() => setModal(null)} />}
|
||||
@ -1999,6 +2011,54 @@ function LocksModal({ me, onClose, onReveal, onFlash, onUnlock }: { me: string;
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- streams (list / switch / merge down / copy up) ---------------- */
|
||||
function StreamsModal({ current, onClose, onFlash, onSwitch, onInteg }: { current: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void; onSwitch: (s: string) => void; onInteg: (s: string, dir: "down" | "up") => void }) {
|
||||
const [streams, setStreams] = useState<{ Stream?: string; Name?: string; Type?: string; Parent?: string }[]>([]);
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [q, setQ] = useState("");
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
p4.streams().then((s) => live && setStreams(s)).catch((e) => { if (live) { onFlash(String(e), true); setStreams([]); } }).finally(() => live && setBusy(false));
|
||||
const k = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", k);
|
||||
return () => { live = false; document.removeEventListener("keydown", k); };
|
||||
}, []); // eslint-disable-line
|
||||
const s = q.trim().toLowerCase();
|
||||
const rows = streams.filter((x) => !s || (x.Stream || "").toLowerCase().includes(s) || (x.Name || "").toLowerCase().includes(s));
|
||||
return (
|
||||
<div className="modal-back" onClick={onClose}>
|
||||
<div className="picker wide" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="picker-head">{I.branch}<h3>{t("Streams")}</h3><span className="ph-sub">{current || t("(not on a stream)")}</span>
|
||||
<button className="x" onClick={onClose}><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg></button>
|
||||
</div>
|
||||
{current && (
|
||||
<div className="rslv-bar">
|
||||
<span style={{ flex: 1, fontSize: 12, color: "var(--muted)" }}>{t("Current stream integration")}</span>
|
||||
<button className="mbtn ghost" onClick={() => onInteg(current, "down")}>{t("Merge down")}</button>
|
||||
<button className="mbtn ghost" onClick={() => onInteg(current, "up")}>{t("Copy up")}</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="ur-filter" style={{ margin: "0 16px 8px" }}>{I.search}<input placeholder={t("Filter streams…")} value={q} onChange={(e) => setQ(e.target.value)} /></div>
|
||||
<div className="srch-list">
|
||||
{busy && <div className="ur-empty"><span className="ldr sm" /> {t("Loading…")}</div>}
|
||||
{!busy && rows.length === 0 && <div className="ur-empty">{t("No streams (this depot may be classic, not stream-based).")}</div>}
|
||||
{rows.map((x) => {
|
||||
const st = x.Stream || "";
|
||||
const on = st === current;
|
||||
return (
|
||||
<div key={st} className={"srch-row" + (on ? " on" : "")}>
|
||||
<span className={"held" + (on ? " locked" : "")} style={{ marginRight: 4 }}>{I.branch}<span>{x.Type || ""}</span></span>
|
||||
<span className="srch-body"><span className="n">{x.Name || st}{on && " · " + t("current")}</span><span className="p">{st}{x.Parent && x.Parent !== "none" ? " ← " + x.Parent : ""}</span></span>
|
||||
{!on && <button className="rslv-act" onClick={() => onSwitch(st)}>{t("Switch")}</button>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- integrate / merge / copy between branches ---------------- */
|
||||
function BranchModal({ scope, onClose, onFlash, onDone }: { scope: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void; onDone: () => void }) {
|
||||
const [op, setOp] = useState<"merge" | "copy" | "integrate">("merge");
|
||||
|
||||
Reference in New Issue
Block a user