diff --git a/src/components/ExportDialog.test.tsx b/src/components/ExportDialog.test.tsx new file mode 100644 index 0000000..6d47eb9 --- /dev/null +++ b/src/components/ExportDialog.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getArmy, getRuleSet } from "../data/gameData"; +import { createList } from "../domain/lists"; +import { buildShareUrl } from "../domain/shareCode"; +import ExportDialog from "./ExportDialog"; + +vi.mock("../domain/shareCode", () => ({ + buildShareUrl: vi.fn(), +})); + +const writeText = vi.fn(); + +function renderDialog() { + const ruleSet = getRuleSet("warmaster-custom")!; + const army = getArmy(ruleSet.id, "dwarfs")!; + const list = createList(ruleSet.id, ruleSet.version, "dwarfs", "Throng of [Dorin]", 2000); + render(); +} + +describe("ExportDialog share links", () => { + beforeEach(() => { + vi.mocked(buildShareUrl).mockResolvedValue("https://warmuster.example/#shared-list"); + writeText.mockReset().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + }); + + it("copies the full sharable URL", async () => { + renderDialog(); + fireEvent.click(screen.getByRole("button", { name: "Copy sharable link" })); + await waitFor(() => + expect(writeText).toHaveBeenCalledWith("https://warmuster.example/#shared-list"), + ); + }); + + it("copies a short Discord Markdown link labeled with the list name", async () => { + renderDialog(); + fireEvent.click(screen.getByRole("button", { name: "Copy Discord link" })); + await waitFor(() => + expect(writeText).toHaveBeenCalledWith( + "[Warmuster - Throng of \\[Dorin\\]](https://warmuster.example/#shared-list)", + ), + ); + }); +}); diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index fe6893f..e17a742 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -13,16 +13,18 @@ interface ExportDialogProps { export default function ExportDialog({ list, army, onClose, onPrint }: ExportDialogProps) { const [copied, setCopied] = useState(false); - const [shareCopied, setShareCopied] = useState(false); + const [copiedLink, setCopiedLink] = useState<"sharable" | "discord" | null>(null); const text = buildTextExport(list, army); - const copyShareLink = async () => { + const copyShareLink = async (format: "sharable" | "discord") => { try { const url = await buildShareUrl(list); - await navigator.clipboard.writeText(url); - setShareCopied(true); - setTimeout(() => setShareCopied(false), 1500); + const listName = list.name.replace(/\s+/g, " ").trim(); + const label = `Warmuster - ${listName}`.replace(/([\\\[\]])/g, "\\$1"); + await navigator.clipboard.writeText(format === "discord" ? `[${label}](${url})` : url); + setCopiedLink(format); + setTimeout(() => setCopiedLink(null), 1500); } catch { // Clipboard unavailable. } @@ -62,8 +64,21 @@ export default function ExportDialog({ list, army, onClose, onPrint }: ExportDia - +

Text for Discord & co.

diff --git a/src/content/info/changelog.md b/src/content/info/changelog.md index a510aee..01e8bdc 100644 --- a/src/content/info/changelog.md +++ b/src/content/info/changelog.md @@ -3,6 +3,7 @@ - Rename a folder from the pencil button on its row, or by double-clicking its name. - A list opened from a share link is filed under **Imports**, creating that folder the first time. - Choose a default folder for shared-list imports under Configuration, or leave them unfiled. This setting is separate for each rule set. +- Export can copy either a full sharable URL or a shorter Discord link labeled with the list name. - Backups carry your folders: exporting and importing on another computer reproduces the same organisation. ## 0.7.0 2026-08-06