Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/components/ExportDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ExportDialog list={list} army={army} onClose={vi.fn()} onPrint={vi.fn()} />);
}

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)",
),
);
});
});
29 changes: 22 additions & 7 deletions src/components/ExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down Expand Up @@ -62,8 +64,21 @@ export default function ExportDialog({ list, army, onClose, onPrint }: ExportDia
<button type="button" className="primary-btn" onClick={() => onPrint("cards")}>
🖨 Print unit cards
</button>
<button type="button" className="primary-btn" onClick={copyShareLink}>
{shareCopied ? "Link copied!" : "🔗 Copy share link"}
<button
type="button"
className="primary-btn"
aria-label="Copy sharable link"
onClick={() => copyShareLink("sharable")}
>
{copiedLink === "sharable" ? "Link copied!" : "🔗 Copy sharable link"}
</button>
<button
type="button"
className="primary-btn"
aria-label="Copy Discord link"
onClick={() => copyShareLink("discord")}
>
{copiedLink === "discord" ? "Link copied!" : "🔗 Copy Discord link"}
</button>
</div>
<h3 className="panel-heading">Text for Discord &amp; co.</h3>
Expand Down
1 change: 1 addition & 0 deletions src/content/info/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down