feat(artifacts): list view, skill packs, badges by exception
The page showed every artifact as a tall card with a blue Install and four text actions, a Shared badge on nearly all of them and 12 cards per section, so 26 artifacts filled ~1900px (5000px on a phone). - List view by default (Grid kept, a per-browser choice): one row per artifact — kind icon, name, one line of description, owner and installs, ONE primary action, everything else in its ⋯ menu / right-click. - Three or more skills of one owner sharing a name prefix (sg-*, vp-*) are one pack row: open it for its skills, or Install all / Install N more after one confirmation (a name already there is left alone). - Badges only for the exception: Private (not shared), Hub only, Auto. - No per-section paging: 30 entries, then Show more. - Scope tabs under the title; category chips scroll on a phone. - Another user's private row (seen by an admin) gets its own section instead of sitting under Shared by others. - "1 installs" → "1 install".
This commit is contained in:
@@ -34,7 +34,6 @@
|
||||
"download": "Download",
|
||||
"noPreview": "This file type cannot be shown here — download it to open it.",
|
||||
"install": "Install",
|
||||
"installs": "{{count}} installs",
|
||||
"installed": "Installed to {{path}}",
|
||||
"installedPlugin": "Installed {{name}} — start a new session (or /reload-plugins) to use it",
|
||||
"rename": "Edit",
|
||||
@@ -197,5 +196,23 @@
|
||||
"unowned.claimed": "Added to your library",
|
||||
"unowned.declined": "Removed from your list",
|
||||
"unowned.assigned": "Owner assigned",
|
||||
"unowned.empty": "Nothing without an owner."
|
||||
"unowned.empty": "Nothing without an owner.",
|
||||
"installs_one": "{{count}} install",
|
||||
"installs_other": "{{count}} installs",
|
||||
"private": "Private",
|
||||
"privateHint": "Not shared: only you (and admins) see it. Share puts it in the Hub.",
|
||||
"showMore": "Show {{count}} more",
|
||||
"view.label": "Layout",
|
||||
"view.list": "List view",
|
||||
"view.grid": "Grid view",
|
||||
"pack.title": "{{prefix}}-* pack",
|
||||
"pack.count_one": "{{count}} skill",
|
||||
"pack.count_other": "{{count}} skills",
|
||||
"pack.installAll": "Install all",
|
||||
"pack.installRest": "Install {{count}} more",
|
||||
"pack.installTitle": "Install {{count}} skills of {{prefix}}-*?",
|
||||
"pack.installBody": "Each one is written into your own account. A skill you already have under the same name is left as it is:",
|
||||
"pack.installed": "Installed {{ok}} · already there {{skipped}} · failed {{failed}}",
|
||||
"section.unshared": "Not shared by their owners",
|
||||
"section.unsharedHint": "Other users' private items — only admins see these"
|
||||
}
|
||||
|
||||
@@ -197,5 +197,20 @@
|
||||
"unowned.claimed": "Đã nhận về thư viện của bạn",
|
||||
"unowned.declined": "Đã bỏ khỏi danh sách của bạn",
|
||||
"unowned.assigned": "Đã gán chủ",
|
||||
"unowned.empty": "Không có mục nào chưa có chủ."
|
||||
"unowned.empty": "Không có mục nào chưa có chủ.",
|
||||
"private": "Riêng tư",
|
||||
"privateHint": "Chưa chia sẻ: chỉ bạn (và admin) thấy. Bấm Chia sẻ để đưa lên Hub.",
|
||||
"showMore": "Xem thêm {{count}}",
|
||||
"view.label": "Cách hiển thị",
|
||||
"view.list": "Dạng danh sách",
|
||||
"view.grid": "Dạng lưới",
|
||||
"pack.title": "Bộ {{prefix}}-*",
|
||||
"pack.count": "{{count}} skill",
|
||||
"pack.installAll": "Cài tất cả",
|
||||
"pack.installRest": "Cài {{count}} còn lại",
|
||||
"pack.installTitle": "Cài {{count}} skill của bộ {{prefix}}-*?",
|
||||
"pack.installBody": "Mỗi skill được ghi vào tài khoản của bạn. Skill nào bạn đã có cùng tên thì giữ nguyên:",
|
||||
"pack.installed": "Đã cài {{ok}} · đã có sẵn {{skipped}} · lỗi {{failed}}",
|
||||
"section.unshared": "Chủ chưa chia sẻ",
|
||||
"section.unsharedHint": "Mục riêng tư của người khác — chỉ admin thấy"
|
||||
}
|
||||
|
||||
+684
-306
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,9 @@
|
||||
* the sandbox cannot show (Office, zip, PDF) offers Download only, the
|
||||
* Add menu closes on Escape or a click outside it, the front page is shelved
|
||||
* by kind until a category is picked, and a card opens its detail sheet.
|
||||
* Rows carry one primary action; Preview, Share and Edit live in the ⋯ menu.
|
||||
* A section shows 30 entries before "Show more", and three or more skills of
|
||||
* one owner sharing a name prefix are one pack row that installs them all.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
@@ -113,6 +116,16 @@ const addMenu = async (user: ReturnType<typeof userEvent.setup>, label: string)
|
||||
await user.click(screen.getByRole("button", { name: label }));
|
||||
};
|
||||
|
||||
/** Run one item of the first (or the given) row's ⋯ menu. */
|
||||
async function menuAction(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
name: string,
|
||||
scope: HTMLElement = document.body
|
||||
) {
|
||||
await user.click(within(scope).getAllByRole("button", { name: "More actions" })[0]!);
|
||||
await user.click(within(screen.getByTestId("artifact-menu")).getByRole("menuitem", { name }));
|
||||
}
|
||||
|
||||
describe("Adding", () => {
|
||||
it("adds the plugins picked from a marketplace link", async () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -218,7 +231,11 @@ describe("Artifacts", () => {
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
expect(await screen.findByRole("button", { name: "Hub" })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("button", { name: /^Share$/ }).length).toBe(2);
|
||||
await user.click(screen.getAllByRole("button", { name: "More actions" })[0]!);
|
||||
expect(
|
||||
within(screen.getByTestId("artifact-menu")).getByRole("menuitem", { name: "Share" })
|
||||
).toBeInTheDocument();
|
||||
await user.keyboard("{Escape}");
|
||||
// One merged list by default; Hub is a filter over it.
|
||||
expect(api.artifacts.list).toHaveBeenLastCalledWith(expect.objectContaining({ scope: "all" }));
|
||||
await user.click(screen.getByRole("button", { name: "Hub" }));
|
||||
@@ -271,20 +288,46 @@ describe("Artifacts", () => {
|
||||
expect(within(screen.getByTestId("half-mine.own")).getByText("my-page")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("pages a long section 12 cards at a time", async () => {
|
||||
it("shows a long section 30 entries at a time, then the rest on Show more", async () => {
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: Array.from({ length: 14 }, (_, i) =>
|
||||
items: Array.from({ length: 34 }, (_, i) =>
|
||||
item({ id: i + 1, kind: "page", title: `p${i + 1}`, files: [] })
|
||||
),
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
await screen.findByText("p1");
|
||||
expect(screen.queryByText("p13")).toBeNull();
|
||||
expect(screen.getByText("Showing 1-12 of 14")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(screen.getByText("p13")).toBeInTheDocument();
|
||||
expect(screen.queryByText("p1")).toBeNull();
|
||||
expect(screen.getByText("p30")).toBeInTheDocument();
|
||||
expect(screen.queryByText("p31")).toBeNull();
|
||||
await user.click(screen.getByRole("button", { name: "Show 4 more" }));
|
||||
expect(screen.getByText("p34")).toBeInTheDocument();
|
||||
expect(screen.getByText("p1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("folds three skills sharing a name prefix into one pack that installs them all", async () => {
|
||||
vi.mocked(api.artifacts.list).mockResolvedValue({
|
||||
items: [
|
||||
item({ id: 1, kind: "skill", title: "sg-qa", source: "sg-qa" }),
|
||||
item({ id: 2, kind: "skill", title: "sg-ship", source: "sg-ship", installed: true }),
|
||||
item({ id: 3, kind: "skill", title: "sg-fix", source: "sg-fix" }),
|
||||
item({ id: 4, kind: "skill", title: "lone", source: "lone" }),
|
||||
],
|
||||
});
|
||||
vi.mocked(api.artifacts.install).mockResolvedValue({ installed: "/h/x" } as never);
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
const pack = await screen.findByTestId("pack-sg");
|
||||
expect(within(pack).getByText("sg-* pack")).toBeInTheDocument();
|
||||
expect(within(pack).getByText("3 skills")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("artifact-1")).toBeNull();
|
||||
expect(screen.getByTestId("artifact-4")).toBeInTheDocument();
|
||||
await user.click(within(pack).getByRole("button", { expanded: false }));
|
||||
expect(within(pack).getByTestId("artifact-1")).toBeInTheDocument();
|
||||
vi.mocked(confirmDialog).mockResolvedValueOnce(true);
|
||||
await user.click(within(pack).getByRole("button", { name: "Install 2 more" }));
|
||||
await waitFor(() => expect(api.artifacts.install).toHaveBeenCalledTimes(2));
|
||||
expect(api.artifacts.install).toHaveBeenCalledWith(1);
|
||||
expect(api.artifacts.install).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("lists who installed an artifact, when and where, from its install count", async () => {
|
||||
@@ -333,7 +376,7 @@ describe("Artifacts", () => {
|
||||
expect(within(card).getByText("Hub only")).toBeInTheDocument();
|
||||
expect(within(card).queryByRole("button", { name: "Delete" })).toBeNull();
|
||||
vi.mocked(confirmDialog).mockResolvedValueOnce(true);
|
||||
await user.click(within(card).getByRole("button", { name: "Remove from Hub" }));
|
||||
await menuAction(user, "Remove from Hub", card);
|
||||
await waitFor(() => expect(api.artifacts.update).toHaveBeenCalledWith(3, { shared: false }));
|
||||
const calls = vi.mocked(confirmDialog).mock.calls;
|
||||
expect(calls[calls.length - 1]?.[0].title).toMatch(/Remove “hub-only” from the Hub/);
|
||||
@@ -436,7 +479,7 @@ describe("Artifacts", () => {
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
await screen.findByText("standup");
|
||||
await user.click(screen.getByRole("button", { name: "Preview" }));
|
||||
await menuAction(user, "Preview");
|
||||
expect(await screen.findByRole("heading", { name: "Daily standup" })).toBeInTheDocument();
|
||||
expect(screen.getByTestId("skill-summary")).toHaveTextContent("/out/report.html");
|
||||
expect(screen.queryByTestId("artifact-preview")).toBeNull();
|
||||
@@ -464,7 +507,7 @@ describe("Artifacts", () => {
|
||||
const user = userEvent.setup();
|
||||
open();
|
||||
await screen.findByText("standup");
|
||||
await user.click(screen.getByRole("button", { name: "Edit" }));
|
||||
await menuAction(user, "Edit");
|
||||
const name = await screen.findByLabelText(/Skill name/);
|
||||
expect(name).toHaveValue("standup");
|
||||
expect(screen.getByLabelText(/Instructions/)).toHaveValue("Old body");
|
||||
|
||||
Reference in New Issue
Block a user