feat: right-click menus in Files/Users/Artifacts; several SSH keys and accounts per host
- Files: right-click a sidebar workspace (Open, Copy path) or home (Open, Open Trash), a path crumb (Open, Download as zip, Paste into, Copy path), a Trash row (Restore, Copy path, Delete forever), the Trash area (Refresh, Empty Trash) and an admin's per-user Trash folder (Open, Empty Trash). - Users: row menu (Edit + Open home in Files for an admin, Copy username/email). - Artifacts: card menu repeating its buttons, plus Copy link for a page. - Git identity: named SSH keys beside the default ones (id_<type>_<name>, max 10, DELETE ?id=); SSH logins per alias (`ssh://<alias>/` → `ssh://<user>@<host>/`, so several AWS CodeCommit accounts share a host), each optionally tied to one key (`agenthub.<from>.sshKey`) — a clone of that URL offers only that key and persists it. Terminal sshCommand follows the keys. Settings → Git: "More SSH keys" + account name/key in the AWS and other-host forms. Docs/API.md, OpenAPI updated.
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
* admin's switch in Settings → AI tools (`ArtifactsHubSettings`) turns Hub
|
||||
* installs off for everyone. Previews of a file are
|
||||
* the server's sandboxed `raw` route in a sandboxed iframe, so nothing a
|
||||
* shared HTML file carries runs on the dashboard's origin.
|
||||
* shared HTML file carries runs on the dashboard's origin. Right-clicking a
|
||||
* card offers the same actions as its buttons, plus copying a page's link.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -43,6 +44,8 @@ import { eventBus } from "../lib/eventBus";
|
||||
import { toast } from "../lib/toast";
|
||||
import { confirmDialog } from "../lib/confirm";
|
||||
import { useDialog } from "../hooks/useDialog";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../components/ContextMenu";
|
||||
import {
|
||||
FileUploadPanel,
|
||||
PluginAddPanel,
|
||||
@@ -325,12 +328,20 @@ function ArtifactCard({
|
||||
}) {
|
||||
const { t } = useTranslation("artifacts");
|
||||
const Icon = KIND_ICON[a.kind];
|
||||
const menu = useContextMenu<null>();
|
||||
const pick = (fn: () => void) => () => {
|
||||
menu.close();
|
||||
fn();
|
||||
};
|
||||
const previewPath = a.kind === "skill" ? "SKILL.md" : (a.files[0]?.path ?? "");
|
||||
const canPreview = (a.kind === "file" || a.kind === "skill") && a.files.length > 0;
|
||||
const btn =
|
||||
"flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-3 hover:text-fg-primary";
|
||||
return (
|
||||
<li
|
||||
className="flex flex-col gap-2 rounded-lg border border-border bg-surface-1 p-3"
|
||||
data-testid={`artifact-${a.id}`}
|
||||
onContextMenu={(e) => menu.open(e, null)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<Icon className="mt-0.5 h-4 w-4 shrink-0 text-fg-muted" />
|
||||
@@ -363,12 +374,8 @@ function ArtifactCard({
|
||||
{t("open")}
|
||||
</a>
|
||||
)}
|
||||
{(a.kind === "file" || a.kind === "skill") && a.files.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className={btn}
|
||||
onClick={() => onPreview(a.kind === "skill" ? "SKILL.md" : (a.files[0]?.path ?? ""))}
|
||||
>
|
||||
{canPreview && (
|
||||
<button type="button" className={btn} onClick={() => onPreview(previewPath)}>
|
||||
<Eye className="h-3 w-3" />
|
||||
{t("preview")}
|
||||
</button>
|
||||
@@ -410,6 +417,47 @@ function ArtifactCard({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{menu.menu && (
|
||||
<ContextMenu x={menu.menu.x} y={menu.menu.y} onClose={menu.close} testId="artifact-menu">
|
||||
{a.kind === "page" && a.url && (
|
||||
<>
|
||||
<MenuItem onSelect={pick(() => window.open(a.url!, "_blank", "noopener,noreferrer"))}>
|
||||
{t("open")}
|
||||
</MenuItem>
|
||||
<MenuItem onSelect={pick(() => void copyText(a.url!))}>{t("copyLink")}</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{canPreview && (
|
||||
<MenuItem onSelect={pick(() => onPreview(previewPath))}>{t("preview")}</MenuItem>
|
||||
)}
|
||||
{a.kind === "file" && a.files.length > 0 && (
|
||||
<MenuItem
|
||||
onSelect={pick(() => {
|
||||
window.location.href = api.artifacts.rawUrl(a.id, a.files[0]?.path, true);
|
||||
})}
|
||||
>
|
||||
{t("download")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{(a.kind === "skill" || a.kind === "plugin") && (
|
||||
<MenuItem disabled={!canInstall} onSelect={pick(onInstall)}>
|
||||
{t("install")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{a.canChange && (
|
||||
<>
|
||||
<MenuSeparator />
|
||||
{server && (
|
||||
<MenuItem onSelect={pick(onShare)}>{t(a.shared ? "unshare" : "share")}</MenuItem>
|
||||
)}
|
||||
<MenuItem onSelect={pick(onRename)}>{t("rename")}</MenuItem>
|
||||
<MenuItem danger onSelect={pick(onDelete)}>
|
||||
{t("delete")}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user