chore: v2.8.72 — update survives npm-rewritten lockfiles; drag artifacts into folders
- update: before the --ff-only merge, put back package-lock.json / client/package-lock.json when they are the ONLY tracked changes (an earlier update's npm install rewrote them; v2.8.71 changed client/package-lock.json and the merge refused on such installs). Any other change still makes the merge refuse. - Artifacts: drag a row or card onto a folder chip to put it there, onto "All folders" to take it out.
This commit is contained in:
@@ -275,5 +275,9 @@
|
||||
"pdf.failed": "This PDF could not be shown. Download it instead.",
|
||||
"pdf.more": "Showing the first {{shown}} of {{total}} pages — download it for the rest.",
|
||||
"exportsSwitch": "Keep a viewable copy of Docs and Slides",
|
||||
"exportsSwitchHint": "When Claude publishes a Claude Docs doc, a deck or a design from an AgentHub session, it is asked to also save a copy (Markdown, PDF or HTML) that the Hub shows to the people it is shared with — they need no claude.ai access. Applies to sessions started afterwards."
|
||||
"exportsSwitchHint": "When Claude publishes a Claude Docs doc, a deck or a design from an AgentHub session, it is asked to also save a copy (Markdown, PDF or HTML) that the Hub shows to the people it is shared with — they need no claude.ai access. Applies to sessions started afterwards.",
|
||||
"folders.moved": "Moved to “{{name}}”",
|
||||
"folders.takenOut": "Taken out of its folder",
|
||||
"folders.dropHint": "Drop an artifact here to put it in this folder",
|
||||
"folders.dropOutHint": "Drop an artifact here to take it out of its folder"
|
||||
}
|
||||
|
||||
@@ -273,5 +273,9 @@
|
||||
"pdf.failed": "Không hiển thị được PDF này. Hãy tải về.",
|
||||
"pdf.more": "Đang hiện {{shown}}/{{total}} trang đầu — tải về để xem hết.",
|
||||
"exportsSwitch": "Giữ bản xem được của Docs và Slides",
|
||||
"exportsSwitchHint": "Khi Claude tạo một Claude Docs, slide hoặc design trong phiên AgentHub, Claude được nhắc lưu thêm một bản sao (Markdown, PDF hoặc HTML) để Hub hiển thị cho người được chia sẻ — họ không cần quyền trên claude.ai. Áp dụng cho các phiên mở sau đó."
|
||||
"exportsSwitchHint": "Khi Claude tạo một Claude Docs, slide hoặc design trong phiên AgentHub, Claude được nhắc lưu thêm một bản sao (Markdown, PDF hoặc HTML) để Hub hiển thị cho người được chia sẻ — họ không cần quyền trên claude.ai. Áp dụng cho các phiên mở sau đó.",
|
||||
"folders.moved": "Đã chuyển vào “{{name}}”",
|
||||
"folders.takenOut": "Đã bỏ khỏi thư mục",
|
||||
"folders.dropHint": "Thả artifact vào đây để đưa vào thư mục này",
|
||||
"folders.dropOutHint": "Thả artifact vào đây để bỏ khỏi thư mục"
|
||||
}
|
||||
|
||||
@@ -131,6 +131,17 @@ const PREVIEWABLE =
|
||||
const PDF = /\.pdf$/i;
|
||||
|
||||
/** What a row can do, worked out once for its card, menu and detail sheet. */
|
||||
/** What a dragged artifact carries: its id, for a folder chip to take. */
|
||||
const DRAG_TYPE = "application/x-agenthub-artifact";
|
||||
const dragProps = (a: Artifact) => ({
|
||||
draggable: true,
|
||||
onDragStart: (e: React.DragEvent) => {
|
||||
e.dataTransfer.setData(DRAG_TYPE, String(a.id));
|
||||
e.dataTransfer.setData("text/plain", a.title);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
},
|
||||
});
|
||||
|
||||
/** Shared at all: with everyone (the Hub) or with chosen people. */
|
||||
const isShared = (a: Artifact) => a.shared || (a.sharedWith?.length ?? 0) > 0;
|
||||
|
||||
@@ -834,6 +845,11 @@ export function Artifacts() {
|
||||
active={activeFolder}
|
||||
counts={(id) => (loaded ?? []).filter((a) => folders.of[a.id] === id).length}
|
||||
onPick={setFolder}
|
||||
onDropItem={(id, fid) => {
|
||||
moveToFolder(id, fid);
|
||||
const f = fid ? folders.folders.find((x) => x.id === fid) : null;
|
||||
toast.success(f ? t("folders.moved", { name: f.name }) : t("folders.takenOut"));
|
||||
}}
|
||||
/>
|
||||
|
||||
{items === null ? (
|
||||
@@ -1539,6 +1555,7 @@ function ArtifactRow({ inPack, ...props }: ItemProps & { inPack?: boolean }) {
|
||||
className="flex items-center gap-3 px-3 py-2.5 transition-colors hover:bg-surface-2/60"
|
||||
data-testid={`artifact-${a.id}`}
|
||||
onContextMenu={(e) => menu.open(e, null)}
|
||||
{...dragProps(a)}
|
||||
>
|
||||
<KindIcon kind={a.kind} />
|
||||
<button
|
||||
@@ -1593,6 +1610,7 @@ function ArtifactCard({ as = "li", ...props }: ItemProps & { as?: "li" | "div" }
|
||||
className="card flex flex-col gap-2.5 p-4 transition-colors hover:border-border-light"
|
||||
data-testid={`artifact-${a.id}`}
|
||||
onContextMenu={(e: React.MouseEvent) => menu.open(e, null)}
|
||||
{...dragProps(a)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<KindIcon kind={a.kind} />
|
||||
@@ -2492,13 +2510,35 @@ function FolderBar({
|
||||
active,
|
||||
counts,
|
||||
onPick,
|
||||
onDropItem,
|
||||
}: {
|
||||
folders: { id: string; name: string }[];
|
||||
active: string;
|
||||
counts: (id: string) => number;
|
||||
onPick: (id: string) => void;
|
||||
/** An artifact dropped on a chip: that folder, or (null, "All folders") none. */
|
||||
onDropItem: (itemId: number, folderId: string | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("artifacts");
|
||||
// The chip an artifact is dragged over ("" = All folders), lit as a target.
|
||||
const [over, setOver] = useState<string | null>(null);
|
||||
const dropProps = (target: string) => ({
|
||||
onDragOver: (e: React.DragEvent) => {
|
||||
if (!e.dataTransfer.types.includes(DRAG_TYPE)) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
if (over !== target) setOver(target);
|
||||
},
|
||||
onDragLeave: () => setOver((cur) => (cur === target ? null : cur)),
|
||||
onDrop: (e: React.DragEvent) => {
|
||||
setOver(null);
|
||||
const id = Number(e.dataTransfer.getData(DRAG_TYPE));
|
||||
if (!Number.isInteger(id) || id <= 0) return;
|
||||
e.preventDefault();
|
||||
onDropItem(id, target || null);
|
||||
},
|
||||
});
|
||||
const lit = (target: string) => (over === target ? " ring-2 ring-accent bg-accent-muted" : "");
|
||||
// `new`: naming a new folder; a folder id: renaming that one.
|
||||
const [editing, setEditing] = useState<"new" | string | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
@@ -2534,8 +2574,10 @@ function FolderBar({
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={!active}
|
||||
className={chip(!active)}
|
||||
className={chip(!active) + lit("")}
|
||||
onClick={() => onPick("")}
|
||||
title={folders.length ? t("folders.dropOutHint") : undefined}
|
||||
{...dropProps("")}
|
||||
>
|
||||
{t("folders.all")}
|
||||
</button>
|
||||
@@ -2544,8 +2586,11 @@ function FolderBar({
|
||||
key={f.id}
|
||||
type="button"
|
||||
aria-pressed={active === f.id}
|
||||
className={chip(active === f.id)}
|
||||
className={chip(active === f.id) + lit(f.id)}
|
||||
onClick={() => onPick(f.id)}
|
||||
title={t("folders.dropHint")}
|
||||
data-testid={`artifact-folder-${f.id}`}
|
||||
{...dropProps(f.id)}
|
||||
>
|
||||
{f.name}
|
||||
<span className="text-fg-muted">{counts(f.id)}</span>
|
||||
|
||||
@@ -506,6 +506,43 @@ describe("Artifacts", () => {
|
||||
localStorage.removeItem("agenthub.artifacts.folders");
|
||||
});
|
||||
|
||||
it("drags an artifact onto a folder to put it there, and onto All folders to take it out", async () => {
|
||||
localStorage.setItem(
|
||||
"agenthub.artifacts.folders",
|
||||
JSON.stringify({ folders: [{ id: "f1", name: "Slides" }], of: {} })
|
||||
);
|
||||
open();
|
||||
const row = await screen.findByTestId("artifact-2");
|
||||
expect(row).toHaveAttribute("draggable", "true");
|
||||
const data = new Map<string, string>();
|
||||
const dataTransfer = {
|
||||
setData: (k: string, v: string) => data.set(k, v),
|
||||
getData: (k: string) => data.get(k) ?? "",
|
||||
get types() {
|
||||
return [...data.keys()];
|
||||
},
|
||||
effectAllowed: "",
|
||||
dropEffect: "",
|
||||
};
|
||||
const stored = () => JSON.parse(localStorage.getItem("agenthub.artifacts.folders")!);
|
||||
fireEvent.dragStart(row, { dataTransfer });
|
||||
const chip = screen.getByTestId("artifact-folder-f1");
|
||||
fireEvent.dragOver(chip, { dataTransfer });
|
||||
expect(chip.className).toMatch(/ring-2/);
|
||||
fireEvent.drop(chip, { dataTransfer });
|
||||
expect(stored().of["2"]).toBe("f1");
|
||||
expect(chip.className).not.toMatch(/ring-2/);
|
||||
// Something that is not an artifact (a file from the desktop) is ignored.
|
||||
fireEvent.drop(chip, {
|
||||
dataTransfer: { ...dataTransfer, getData: () => "", types: ["Files"] },
|
||||
});
|
||||
expect(stored().of["2"]).toBe("f1");
|
||||
fireEvent.dragStart(row, { dataTransfer });
|
||||
fireEvent.drop(screen.getByRole("button", { name: "All folders" }), { dataTransfer });
|
||||
expect(stored().of["2"]).toBeUndefined();
|
||||
localStorage.removeItem("agenthub.artifacts.folders");
|
||||
});
|
||||
|
||||
it("keeps a shared row in the Hub when its owner deletes it, and removes it from there on ask", async () => {
|
||||
vi.mocked(api.artifacts.settings).mockResolvedValue({
|
||||
serverMode: true,
|
||||
|
||||
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.8.71 (rebuild client to match)
|
||||
UI build v2.8.72 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user