|
|
|
|
@ -51,6 +51,11 @@ const I = {
|
|
|
|
|
unlock: <svg viewBox="0 0 24 24" fill="none"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" strokeWidth="1.6"/><path d="M8 11V8a4 4 0 0 1 7.5-1.9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
|
|
|
|
shelf: <svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
|
|
|
|
folder: <svg viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" stroke="currentColor" strokeWidth="1.6"/></svg>,
|
|
|
|
|
power: <svg viewBox="0 0 24 24" fill="none"><path d="M12 3v9M7.5 6.5a7 7 0 1 0 9 0" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>,
|
|
|
|
|
up: <svg viewBox="0 0 24 24" fill="none"><path d="M12 20V6M6 12l6-6 6 6" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
|
|
|
|
gear: <svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.6"/><path d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.3 1a7 7 0 0 0-1.7-1l-.4-2.6H10.5l-.4 2.6a7 7 0 0 0-1.7 1l-2.3-1-2 3.4L6 11a7 7 0 0 0 0 2l-2 1.5 2 3.4 2.3-1a7 7 0 0 0 1.7 1l.4 2.6h3.5l.4-2.6a7 7 0 0 0 1.7-1l2.3 1 2-3.4-2-1.5c.06-.33.1-.66.1-1Z" stroke="currentColor" strokeWidth="1.3"/></svg>,
|
|
|
|
|
info: <svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6"/><path d="M12 11v5M12 8h.01" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></svg>,
|
|
|
|
|
scan: <svg viewBox="0 0 24 24" fill="none"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2M4 12h16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
|
|
|
|
ue: <svg viewBox="0 0 1280 1280"><path fillRule="evenodd" clipRule="evenodd" fill="currentColor" d="M640,1280c353.46,0,640-286.54,640-640S993.46,0,640,0S0,286.54,0,640S286.54,1280,640,1280z M803.7,995.81c156.5-73.92,205.56-210.43,216.6-263.61c-57.22,58.6-120.53,118-163.11,76.88c0,0-2.33-219.45-2.33-309.43c0-121,114.75-211.18,114.75-211.18c-63.11,11.24-138.89,33.71-219.33,112.65c-7.26,7.2-14.14,14.76-20.62,22.67c-34.47-26.39-79.14-18.48-79.14-18.48c24.14,13.26,48.23,51.88,48.23,83.85v314.26c0,0-52.63,46.3-93.19,46.3c-9.14,0.07-18.17-2.05-26.33-6.18c-8.16-4.13-15.21-10.15-20.56-17.56c-3.21-4.19-5.87-8.78-7.91-13.65V424.07c-11.99,9.89-52.51,18.04-52.51-49.22c0-41.79,30.11-91.6,83.73-122.15c-73.63,11.23-142.59,43.04-198.92,91.76c-42.8,36.98-77.03,82.85-100.31,134.4c-23.28,51.55-35.06,107.55-34.51,164.12c0,0,39.21-122.51,88.32-133.83c7.15-1.88,14.65-2.07,21.89-0.54c7.24,1.53,14.02,4.72,19.81,9.34c5.79,4.61,10.41,10.51,13.51,17.23c3.1,6.72,4.59,14.07,4.34,21.46V844.3c0,29.16-18.8,35.53-36.17,35.22c-11.77-0.83-23.4-3.02-34.66-6.53c35.86,48.53,82.46,88.12,136.15,115.66c53.69,27.54,113.03,42.29,173.37,43.1l106.05-106.6L803.7,995.81z"/></svg>,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@ -303,6 +308,7 @@ type ModalState =
|
|
|
|
|
| { kind: "settings" }
|
|
|
|
|
| { kind: "users" }
|
|
|
|
|
| { kind: "search" }
|
|
|
|
|
| { kind: "locks" }
|
|
|
|
|
| { kind: "blame"; spec: string; name: string }
|
|
|
|
|
| { kind: "diff"; title: string; text: string }
|
|
|
|
|
| { kind: "about" };
|
|
|
|
|
@ -1018,17 +1024,18 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|
|
|
|
} catch (e) { flash(String(e), true); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const menus: Record<string, { label: string; kb?: string; ext?: boolean; act?: () => 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("") }],
|
|
|
|
|
Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", act: rescan }, { label: t("Get Latest"), kb: "Ctrl+G", act: getLatest }, { label: t("Commit (local)"), act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", act: pushAll }, ...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), act: () => setResolveOpen(true) }] : []), { label: t("Revert All…"), act: revertAll }, { label: t("Refresh"), kb: "F5", act: () => refresh() }],
|
|
|
|
|
const menus: Record<string, { label: string; kb?: string; ext?: boolean; icon?: ReactNode; act?: () => void }[]> = {
|
|
|
|
|
File: [{ label: t("Switch workspace…"), icon: I.monitor, act: openWorkspaces }, { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }, { label: t("Disconnect"), icon: I.power, act: onDisconnect }],
|
|
|
|
|
Connection: [{ label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }, { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }],
|
|
|
|
|
Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", icon: I.scan, act: rescan }, { label: t("Get Latest"), kb: "Ctrl+G", icon: I.sync, act: getLatest }, { label: t("Commit (local)"), icon: I.check, act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", icon: I.up, act: pushAll }, ...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), icon: I.hex, act: () => setResolveOpen(true) }] : []), { label: t("Revert All…"), icon: I.revert, act: revertAll }, { label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }],
|
|
|
|
|
Window: [
|
|
|
|
|
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", act: () => openDock("log") },
|
|
|
|
|
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", act: () => openDock("terminal") },
|
|
|
|
|
...(uproject ? [{ label: (dockOpen && dockTab === "unreal" ? "✓ " : "") + t("Unreal Log"), act: () => openDock("unreal") }] : []),
|
|
|
|
|
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, act: () => openDock("log") },
|
|
|
|
|
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, act: () => openDock("terminal") },
|
|
|
|
|
...(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…"), act: () => setModal({ kind: "search" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", act: startBuild }, { label: t("People & Roles…"), act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), act: () => setModal({ kind: "settings" }) }],
|
|
|
|
|
Help: [{ label: t("About"), act: () => setModal({ kind: "about" }) }],
|
|
|
|
|
Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { 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" }) }],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@ -1042,6 +1049,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|
|
|
|
<div className="dropdown">
|
|
|
|
|
{items.map((it) => (
|
|
|
|
|
<div key={it.label} className={"di" + (it.ext ? " ext" : "")} onClick={() => it.act?.()}>
|
|
|
|
|
{it.icon && <span className="di-ic">{it.icon}</span>}
|
|
|
|
|
{it.label}{it.kb && <span className="kb">{it.kb}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
@ -1173,8 +1181,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="chhead">
|
|
|
|
|
<span className={"chk" + (allChecked ? "" : " empty")} onClick={toggleAll} title={allChecked ? t("Deselect all") : t("Select all")}
|
|
|
|
|
style={{ width: 16, height: 16, minWidth: 16, flex: "0 0 16px", boxSizing: "border-box", alignSelf: "center" }}>{allChecked ? I.check : null}</span>
|
|
|
|
|
<span className={"chk" + (allChecked ? "" : " off")} onClick={toggleAll} title={allChecked ? t("Deselect all") : t("Select all")}>{allChecked ? I.check : null}</span>
|
|
|
|
|
<span>{t("{a} of {b} selected", { a: checked.size, b: view.length })}</span>
|
|
|
|
|
<button className={"scanbtn" + ((busy || scanning) ? " spinning" : "")} style={{ marginLeft: "auto" }} onClick={() => refresh()} disabled={busy || scanning} title={t("Re-read open files (p4 opened)")}>
|
|
|
|
|
{I.sync}<span>{scanning ? t("Scanning…") : t("Refresh")}</span>
|
|
|
|
|
@ -1278,6 +1285,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|
|
|
|
onClose={() => setModal(null)} onPickWorkspace={switchWorkspace} onBrowse={browseTo} onChoose={chooseScope} onDisconnect={onDisconnect} />}
|
|
|
|
|
{modal?.kind === "users" && <UsersRoles me={info?.userName || ""} onClose={() => setModal(null)} onFlash={flash} />}
|
|
|
|
|
{modal?.kind === "search" && <SearchModal scope={activePath} onClose={() => setModal(null)} onOpenEditor={(dp) => p4.openInEditor(dp, effEditorId).catch((e) => flash(String(e), true))} onReveal={reveal} onCopy={(dp) => copyText(dp, t("Path"))} />}
|
|
|
|
|
{modal?.kind === "locks" && <LocksModal me={info?.userName || ""} onClose={() => setModal(null)} onReveal={reveal} onFlash={flash} onUnlock={(dp) => lockFiles([dp], false)} />}
|
|
|
|
|
{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)} />}
|
|
|
|
|
{resolveOpen && <ResolveModal files={needResolve} onResolve={doResolve} onClose={() => setResolveOpen(false)} />}
|
|
|
|
|
@ -1911,6 +1919,54 @@ function SearchModal({ scope, onClose, onOpenEditor, onReveal, onCopy }: { scope
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------- file locks viewer — who has what locked/open across the depot ---------------- */
|
|
|
|
|
function LocksModal({ me, onClose, onReveal, onFlash, onUnlock }: { me: string; onClose: () => void; onReveal: (dp: string) => void; onFlash: (t: string, e?: boolean) => void; onUnlock: (dp: string) => void }) {
|
|
|
|
|
const [files, setFiles] = useState<OpenedFile[]>([]);
|
|
|
|
|
const [busy, setBusy] = useState(true);
|
|
|
|
|
const [q, setQ] = useState("");
|
|
|
|
|
const [lockedOnly, setLockedOnly] = useState(true);
|
|
|
|
|
const load = () => { setBusy(true); p4.openedAll("").then(setFiles).catch((e) => { onFlash(String(e), true); setFiles([]); }).finally(() => setBusy(false)); };
|
|
|
|
|
useEffect(() => { load(); const k = (e: KeyboardEvent) => e.key === "Escape" && onClose(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, []); // eslint-disable-line
|
|
|
|
|
const isLocked = (f: OpenedFile) => (f as Record<string, unknown>).ourLock !== undefined;
|
|
|
|
|
const s = q.trim().toLowerCase();
|
|
|
|
|
const rows = files.filter((f) => (!lockedOnly || isLocked(f)) && (!s || (f.depotFile || "").toLowerCase().includes(s) || (f.user || "").toLowerCase().includes(s)));
|
|
|
|
|
const lockedCount = files.filter(isLocked).length;
|
|
|
|
|
return (
|
|
|
|
|
<div className="modal-back" onClick={onClose}>
|
|
|
|
|
<div className="picker wide" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<div className="picker-head">{I.lock}<h3>{t("File Locks")}</h3>
|
|
|
|
|
<span className="ph-sub">{t("{n} open · {m} locked", { n: files.length, m: lockedCount })}</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="rslv-bar" style={{ borderBottom: "1px solid var(--border-soft)" }}>
|
|
|
|
|
<div className="ur-filter" style={{ flex: 1, padding: 0, border: "none" }}>{I.search}<input placeholder={t("Filter by file or user…")} value={q} onChange={(e) => setQ(e.target.value)} /></div>
|
|
|
|
|
<button className={"rslv-act" + (lockedOnly ? " on" : "")} onClick={() => setLockedOnly((v) => !v)}>{lockedOnly ? t("Locked only") : t("All open")}</button>
|
|
|
|
|
<button className="rslv-act" onClick={load} title={t("Refresh")}>{I.sync}</button>
|
|
|
|
|
</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">{lockedOnly ? t("No locked files.") : t("No files open by anyone.")}</div>}
|
|
|
|
|
{rows.map((f) => {
|
|
|
|
|
const dp = f.depotFile || "";
|
|
|
|
|
const sp = splitPath(dp);
|
|
|
|
|
const locked = isLocked(f);
|
|
|
|
|
const mine = (f.user || "").toLowerCase() === me.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
<div key={dp + (f.client || "")} className="srch-row">
|
|
|
|
|
<span className={"held" + (locked ? " locked" : "")} style={{ marginRight: 4 }}>{locked ? I.lock : I.unlock}</span>
|
|
|
|
|
<span className="srch-body"><span className="n">{sp.name}</span><span className="p">{sp.dir}</span></span>
|
|
|
|
|
<span className="lk-who">{f.user}{mine ? " · " + t("you") : ""}<span className="lk-client">{f.client}</span></span>
|
|
|
|
|
{locked && mine && <button className="rslv-act" title={t("Unlock")} onClick={() => { onUnlock(dp); setTimeout(load, 400); }}>{t("Unlock")}</button>}
|
|
|
|
|
<button className="srch-act" title={t("Open in Explorer")} onClick={() => onReveal(dp)}>{I.folder}</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------- blame / annotate ---------------- */
|
|
|
|
|
function BlameModal({ spec, name, onClose, onFlash }: { spec: string; name: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void }) {
|
|
|
|
|
const [text, setText] = useState<string | null>(null);
|
|
|
|
|
@ -2209,7 +2265,7 @@ function Preview({ file, onRevert, onFlash, editorId, editorName, onBlame, onDif
|
|
|
|
|
{codeFile && onBlame && (
|
|
|
|
|
<span className="gbtn icon" title={t("Blame — who changed each line")} onClick={() => onBlame(spec || dp, name)}>{I.people}</span>
|
|
|
|
|
)}
|
|
|
|
|
{hist && onDiffPrev && (
|
|
|
|
|
{hist && onDiffPrev && codeFile && (
|
|
|
|
|
<span className="gbtn icon" title={t("Diff against the previous revision")} onClick={() => onDiffPrev(file)}>{I.clock}</span>
|
|
|
|
|
)}
|
|
|
|
|
{!hist && <span className="gbtn" onClick={() => onRevert(file)}>{I.revert}{t("Revert")}</span>}
|
|
|
|
|
|