diff --git a/client/src/components/lanes/FileExplorer.tsx b/client/src/components/lanes/FileExplorer.tsx index 6ed9bd0d..7387ea9d 100644 --- a/client/src/components/lanes/FileExplorer.tsx +++ b/client/src/components/lanes/FileExplorer.tsx @@ -102,6 +102,11 @@ type Clip = { place: FsPlace; paths: string[]; cut: boolean }; // The clipboard outlives the dialog and a workspace switch, like a desktop's. let clipboard: Clip | null = null; +/** A menu item's handler that closes its menu first. */ +const pick = (m: { close: () => void }, fn: () => void) => () => { + m.close(); + fn(); +}; /** The workspace id of a place, or null for a home. */ const laneOf = (t: FsPlace) => (isHomePlace(t) ? null : typeof t === "number" ? t : t.lane); /** A workspace's own folder (not a session worktree or an extra folder), or a home. */ @@ -279,6 +284,9 @@ export function FileBrowser({ const fileInput = useRef(null); const folderInput = useRef(null); const ctx = useContextMenu(); + // The sidebar's places and the path bar's crumbs have their own menus. + const placeCtx = useContextMenu<{ home: string } | { lane: Lane }>(); + const crumbCtx = useContextMenu(); // A new caller target (another workspace's Files tab, the page's URL) — // not the echo of our own `go()` through the page's URL, which would wipe @@ -668,6 +676,18 @@ export function FileBrowser({ const item = trash.find((i) => i.id === id); await run(() => api.files.purge(target, id, item?.store), t("fx.purged")); }; + /** Empty one user's whole Trash from the Trash root — asked first. */ + const emptyTrashOf = async (home: string, count: number) => { + const ok = await confirmDialog({ + title: t("fx.emptyTrashTitle"), + message: t("fx.emptyTrashBody", { + count, + where: home === "me" ? t("fx.myTrash") : t("fx.trashOf", { name: homeName(home) }), + }), + confirmLabel: t("fx.emptyTrash"), + }); + if (ok) await run(() => api.files.purge({ home }, "all"), t("fx.purged")); + }; const restoreItem = (id: string) => { const item = trash.find((i) => i.id === id); void run(() => api.files.restore(target, id, item?.store), t("fx.restored")); @@ -772,6 +792,7 @@ export function FileBrowser({ @@ -442,7 +507,7 @@ export function GitIdentitySettings() { ))} {id.writable && (
{ e.preventDefault(); void saveAws(); @@ -468,11 +533,22 @@ export function GitIdentitySettings() { ))} + setAws({ ...aws, alias: e.target.value })} + className={`${INPUT} font-mono`} + /> + {keySelect(aws.key, (key) => setAws({ ...aws, key }), "rsa")}
)} +
{t("gitIdentity.awsMany")}
)} @@ -480,12 +556,119 @@ export function GitIdentitySettings() {
{t("gitIdentity.awsReady")} {awsRows.map((r) => ( - git clone ssh://{r.host}/v1/repos/<repo> + + git clone ssh://{r.alias ?? r.host}/v1/repos/<repo> + ))}
)} + {(named.length > 0 || id.writable) && ( +
+
+
+
+ {t("gitIdentity.moreKeysTitle")} +
+
{t("gitIdentity.moreKeysSub")}
+
+ {id.writable && !newKey && ( + + )} +
+ {newKey && ( +
{ + e.preventDefault(); + void makeNamed(); + }} + > + setNewKey({ ...newKey, name: e.target.value })} + className={`${INPUT} font-mono`} + /> + + + +
+ )} + {named.map((k) => ( +
+
+ + {k.name} + + + {k.type === "rsa" ? "RSA" : "ED25519"} + + + {k.fingerprint} + + + {id.writable && ( + + )} +
+