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:
@@ -6,10 +6,11 @@
|
||||
* a plugin is picked from the list its marketplace link answers (a link the
|
||||
* server cannot read opens the by-hand form), a skill is uploaded from one
|
||||
* Markdown file under the name the user types, or written in the dialog.
|
||||
* A card's right-click menu runs the same actions (Preview, Delete).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { Artifacts } from "../Artifacts";
|
||||
@@ -81,7 +82,13 @@ beforeEach(() => {
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: [
|
||||
item({}),
|
||||
item({ id: 2, kind: "skill", title: "deploy", source: "deploy", files: [{ path: "SKILL.md", size: 3 }] }),
|
||||
item({
|
||||
id: 2,
|
||||
kind: "skill",
|
||||
title: "deploy",
|
||||
source: "deploy",
|
||||
files: [{ path: "SKILL.md", size: 3 }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
vi.mocked(confirmDialog).mockReset();
|
||||
@@ -145,7 +152,9 @@ describe("Adding", () => {
|
||||
await addMenu(user, "Skill");
|
||||
const md = new File(["# Steps"], "notes.md", { type: "text/markdown" });
|
||||
await user.upload(screen.getByTestId("skill-files"), md);
|
||||
expect(await screen.findByTestId("skill-shape")).toHaveTextContent("becomes the skill's SKILL.md");
|
||||
expect(await screen.findByTestId("skill-shape")).toHaveTextContent(
|
||||
"becomes the skill's SKILL.md"
|
||||
);
|
||||
const name = screen.getByLabelText(/Skill name/);
|
||||
await waitFor(() => expect(name).toHaveValue("notes"));
|
||||
await user.clear(name);
|
||||
@@ -229,4 +238,19 @@ describe("Artifacts", () => {
|
||||
expect(frame.getAttribute("sandbox")).toBe("");
|
||||
expect(frame.getAttribute("src")).toBe("/api/artifacts/1/raw?path=report.html");
|
||||
});
|
||||
|
||||
it("opens a card's actions on right-click", async () => {
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
await screen.findByText("report.html");
|
||||
fireEvent.contextMenu(screen.getByTestId("artifact-1"));
|
||||
const menu = screen.getByTestId("artifact-menu");
|
||||
expect(within(menu).getByRole("menuitem", { name: "Delete" })).toBeInTheDocument();
|
||||
expect(within(menu).queryByRole("menuitem", { name: "Install" })).toBeNull();
|
||||
await user.click(within(menu).getByRole("menuitem", { name: "Preview" }));
|
||||
expect(screen.queryByTestId("artifact-menu")).toBeNull();
|
||||
expect((await screen.findByTestId("artifact-preview")).getAttribute("src")).toBe(
|
||||
"/api/artifacts/1/raw?path=report.html"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
* with their own password typed (the confirm stays disabled until then), sees
|
||||
* a wrong password reported in the edit dialog, opens ONE Edit per row (no
|
||||
* separate role/password/workspace/disable buttons any more), and sees the
|
||||
* display name and email under a username.
|
||||
* display name and email under a username; right-clicking a row opens that
|
||||
* user's home in Files (an admin) — an analyst's menu only copies.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import type { AuthState, ServerUserUsage } from "../../lib/api";
|
||||
|
||||
let authState: AuthState = { mode: "local" };
|
||||
@@ -60,12 +61,17 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
|
||||
import { Users } from "../Users";
|
||||
|
||||
function FilesProbe() {
|
||||
return <div data-testid="files">{useLocation().search}</div>;
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={["/users"]}>
|
||||
<Routes>
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/" element={<div data-testid="home" />} />
|
||||
<Route path="/files" element={<FilesProbe />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -192,4 +198,23 @@ describe("Users page", () => {
|
||||
expect(screen.getByText("@mia")).toBeTruthy();
|
||||
expect(screen.getByText("mia@x.vn")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens a row's menu on right-click: Open home for an admin, copy only for an analyst", async () => {
|
||||
authState = { mode: "server", user: { id: 1, username: "boss", role: "admin" } };
|
||||
renderPage();
|
||||
fireEvent.contextMenu((await screen.findByText("mia")).closest("tr")!);
|
||||
const menu = screen.getByTestId("users-row-menu");
|
||||
expect(within(menu).getByRole("menuitem", { name: "Copy username" })).toBeTruthy();
|
||||
fireEvent.click(within(menu).getByRole("menuitem", { name: "Open home in Files" }));
|
||||
expect((await screen.findByTestId("files")).textContent).toBe("?home=mia");
|
||||
});
|
||||
|
||||
it("gives an analyst's row menu no admin actions", async () => {
|
||||
authState = { mode: "server", user: { id: 3, username: "ana", role: "analyst" } };
|
||||
renderPage();
|
||||
fireEvent.contextMenu((await screen.findByText("mia")).closest("tr")!);
|
||||
const menu = screen.getByTestId("users-row-menu");
|
||||
expect(within(menu).queryByRole("menuitem", { name: "Open home in Files" })).toBeNull();
|
||||
expect(within(menu).queryByRole("menuitem", { name: "Edit" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user