Submit options (-r / revert-unchanged / -e shelved) + .p4ignore editor
- Pending changelist menu: Submit & keep checked out (-r), Submit reverting unchanged, Submit shelved (-e) - Tools -> Edit .p4ignore: read/write workspace .p4ignore with an Unreal template - Backend p4 submit -r/-f revertunchanged/-e, .p4ignore read/write Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
58
src/App.tsx
58
src/App.tsx
@ -315,6 +315,7 @@ type ModalState =
|
||||
| { kind: "streams" }
|
||||
| { kind: "jobs" }
|
||||
| { kind: "clean" }
|
||||
| { kind: "ignore" }
|
||||
| { kind: "filelog"; depot: string; name: string }
|
||||
| { kind: "client"; name: string; isNew: boolean }
|
||||
| { kind: "blame"; spec: string; name: string }
|
||||
@ -914,6 +915,19 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
try { await p4.shelve(change); flash(t("Shelved #{n} on the server.", { n: change })); }
|
||||
catch (e) { flash(String(e), true); } finally { setBusy(false); }
|
||||
}
|
||||
// submit variants: reopen after submit (-r), revert-unchanged, or submit a shelf (-e)
|
||||
async function submitWithOpts(change: string, reopen: boolean, revertUnchanged: boolean) {
|
||||
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.submitOpts(change, reopen, revertUnchanged); flash(t("Submitted changelist #{n}.", { n: change })); clearDraftIfCommitted(); await refresh(); await loadHistory(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
}
|
||||
async function submitShelved(change: string) {
|
||||
if (!(await confirm({ title: t("Submit shelved #{n}?", { n: change }), body: t("The shelved files of #{n} are submitted directly from the server.", { n: change }), confirm: t("Submit") }))) return;
|
||||
setBusy(true);
|
||||
try { await p4.submitShelved(change); flash(t("Submitted shelved changelist #{n}.", { n: change })); await refresh(); await loadHistory(); }
|
||||
catch (e) { flash(String(e), true); setBusy(false); }
|
||||
}
|
||||
async function unshelveCL(change: string) {
|
||||
setBusy(true);
|
||||
try { await p4.unshelve(change); flash(t("Unshelved #{n} into the workspace.", { n: change })); await refresh(); }
|
||||
@ -1082,7 +1096,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: 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" }) }],
|
||||
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: t("Edit .p4ignore…"), icon: I.gear, act: () => setModal({ kind: "ignore" }) }, { 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" }) }],
|
||||
};
|
||||
|
||||
@ -1195,8 +1209,11 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
<button className="cl-more" onClick={(e) => openCtx(e, [
|
||||
{ label: t("Edit description"), act: () => editDesc(cl) },
|
||||
{ label: t("Attach job…"), icon: I.log, act: () => attachJob(ch) },
|
||||
{ label: t("Submit & keep checked out (-r)"), icon: I.up, act: () => submitWithOpts(ch, true, false) },
|
||||
{ label: t("Submit, reverting unchanged"), icon: I.up, act: () => submitWithOpts(ch, false, true) },
|
||||
{ label: t("Shelve (share on server)"), icon: I.shelf, act: () => shelveCL(ch) },
|
||||
{ label: t("Unshelve into workspace"), act: () => unshelveCL(ch) },
|
||||
{ label: t("Submit shelved (-e)"), icon: I.up, act: () => submitShelved(ch) },
|
||||
{ label: t("Delete shelved files"), danger: true, act: () => deleteShelfCL(ch) },
|
||||
{ label: t("Uncommit (back to working)"), act: () => uncommit(cf) },
|
||||
{ label: t("Discard changes · {n} files", { n: cf.length }), danger: true, act: () => discard(cf) },
|
||||
@ -1342,6 +1359,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{modal?.kind === "streams" && <StreamsModal current={String(info?.Stream || "")} onClose={() => setModal(null)} onFlash={flash} onSwitch={doSwitchStream} onInteg={doStreamInteg} />}
|
||||
{modal?.kind === "jobs" && <JobsModal onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "clean" && <CleanModal scope={activePath} onClose={() => setModal(null)} onFlash={flash} onDone={() => { setModal(null); refresh(); }} />}
|
||||
{modal?.kind === "ignore" && <IgnoreModal onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "filelog" && <FilelogModal depot={modal.depot} name={modal.name} onClose={() => setModal(null)} onFlash={flash} onShowDiff={(title, text) => setModal({ kind: "diff", title, text })} />}
|
||||
{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} />}
|
||||
@ -2025,6 +2043,44 @@ function LocksModal({ me, onClose, onReveal, onFlash, onUnlock }: { me: string;
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- .p4ignore editor ---------------- */
|
||||
const P4IGNORE_TEMPLATE = "# Unreal Engine — keep generated / local files out of Perforce\nBinaries/\nBuild/\nDerivedDataCache/\nIntermediate/\nSaved/\n.vs/\n*.sln\n*.suo\n*.opensdf\n*.sdf\n*.VC.db\n*.VC.opendb\n";
|
||||
function IgnoreModal({ onClose, onFlash }: { onClose: () => void; onFlash: (t: string, e?: boolean) => void }) {
|
||||
const [text, setText] = useState("");
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
p4.ignoreRead().then((s) => { if (live) { setText(s); setBusy(false); } }).catch((e) => { if (live) { onFlash(String(e), true); setBusy(false); } });
|
||||
const k = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", k);
|
||||
return () => { live = false; document.removeEventListener("keydown", k); };
|
||||
}, []); // eslint-disable-line
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try { await p4.ignoreWrite(text); onFlash(t(".p4ignore saved.")); onClose(); }
|
||||
catch (e) { onFlash(String(e), true); }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
return (
|
||||
<div className="modal-back" onClick={onClose}>
|
||||
<div className="picker wide blame" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="picker-head">{I.gear}<h3>.p4ignore</h3><span className="ph-sub">{t("at the workspace root")}</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>
|
||||
<div className="tm-hint">{t("Files matching these patterns are ignored by reconcile / add. One pattern per line. Requires P4IGNORE to be configured (usually .p4ignore).")}
|
||||
{!busy && !text.trim() && <> <button className="ce-btn" style={{ display: "inline-flex", marginLeft: 6 }} onClick={() => setText(P4IGNORE_TEMPLATE)}>{t("Insert Unreal template")}</button></>}</div>
|
||||
{busy ? <div className="ur-empty" style={{ flex: 1 }}><span className="ldr" /> {t("Loading…")}</div>
|
||||
: <textarea className="code-edit" value={text} spellCheck={false} onChange={(e) => setText(e.target.value)} placeholder={"Binaries/\nIntermediate/\nSaved/\n*.tmp"} />}
|
||||
<div className="modal-actions" style={{ padding: "12px 16px", borderTop: "1px solid var(--border-soft)" }}>
|
||||
<button className="mbtn ghost" onClick={onClose} disabled={saving}>{t("Cancel")}</button>
|
||||
<button className="mbtn primary" onClick={save} disabled={saving || busy}>{saving ? <span className="ldr sm" /> : null}{t("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- clean workspace (make it match the depot) ---------------- */
|
||||
function CleanModal({ scope, onClose, onFlash, onDone }: { scope: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void; onDone: () => void }) {
|
||||
const [files, setFiles] = useState<OpenedFile[]>([]);
|
||||
|
||||
Reference in New Issue
Block a user