Skip to content

Commit d0b35a1

Browse files
Add CSV export for bookmarks (#266)
* Initial plan * Add CSV export functionality for bookmarks Co-authored-by: alexthemitchell <[email protected]> * Address PR review comments - extract shared type, fix CSV escaping, remove unused mock Co-authored-by: alexthemitchell <[email protected]> * Address second round of PR review comments - improve date mocking, aria-label, docs, and RFC 4180 compliance Co-authored-by: alexthemitchell <[email protected]> * Add formula injection protection and align with codebase CSV conventions Co-authored-by: alexthemitchell <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexthemitchell <[email protected]>
1 parent 19ced9a commit d0b35a1

File tree

5 files changed

+505
-11
lines changed

5 files changed

+505
-11
lines changed

src/panels/Bookmarks.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState, useEffect, useMemo, useRef } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { notify } from "../lib/notifications";
4+
import { downloadBookmarksCSV } from "../utils/bookmark-import-export";
45
import { formatFrequency } from "../utils/frequency";
56
import { generateBookmarkId } from "../utils/id";
7+
import type { Bookmark } from "../types/bookmark";
68

79
/**
810
* Bookmarks panel/page for frequency management
@@ -24,16 +26,6 @@ import { generateBookmarkId } from "../utils/id";
2426
* TODO: Migrate to IndexedDB for better performance with 10k+ entries
2527
*/
2628

27-
interface Bookmark {
28-
id: string;
29-
frequency: number; // Hz
30-
name: string;
31-
tags: string[];
32-
notes: string;
33-
createdAt: number; // timestamp
34-
lastUsed: number; // timestamp
35-
}
36-
3729
interface BookmarksProps {
3830
isPanel?: boolean; // True when rendered as a side panel, false for full-page route
3931
}
@@ -261,6 +253,16 @@ function Bookmarks({ isPanel = false }: BookmarksProps): React.JSX.Element {
261253
});
262254
};
263255

256+
const handleExport = (): void => {
257+
downloadBookmarksCSV(bookmarks);
258+
notify({
259+
message: `Exported ${bookmarks.length} bookmark${bookmarks.length !== 1 ? "s" : ""} to CSV`,
260+
sr: "polite",
261+
visual: true,
262+
tone: "success",
263+
});
264+
};
265+
264266
const containerClass = isPanel ? "panel-container" : "page-container";
265267
const showForm = isAdding || editingId !== null;
266268
const bookmarkToDelete = pendingDeleteId
@@ -371,8 +373,16 @@ function Bookmarks({ isPanel = false }: BookmarksProps): React.JSX.Element {
371373
)}
372374
</section>
373375

374-
<section aria-label="Add Bookmark">
376+
<section aria-label="Bookmark Actions">
375377
<button onClick={handleAdd}>Add Bookmark</button>
378+
{bookmarks.length > 0 && (
379+
<button
380+
onClick={handleExport}
381+
aria-label="Export bookmarks to CSV"
382+
>
383+
Export CSV
384+
</button>
385+
)}
376386
</section>
377387
</>
378388
)}

src/panels/__tests__/Bookmarks.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,45 @@ describe("Bookmarks", () => {
304304
expect(arg).toMatch(/\/monitor\?frequency=162550000/);
305305
});
306306
});
307+
308+
describe("CSV Export", () => {
309+
it("does not show export button when no bookmarks exist", () => {
310+
render(
311+
<BrowserRouter>
312+
<Bookmarks />
313+
</BrowserRouter>,
314+
);
315+
316+
expect(
317+
screen.queryByRole("button", { name: /export/i }),
318+
).not.toBeInTheDocument();
319+
});
320+
321+
it("shows export button when bookmarks exist", async () => {
322+
const bookmarks = [
323+
{
324+
id: "bm-1",
325+
frequency: 100000000,
326+
name: "FM Radio",
327+
tags: ["fm"],
328+
notes: "",
329+
createdAt: Date.now(),
330+
lastUsed: Date.now(),
331+
},
332+
];
333+
localStorageMock.setItem("rad.io:bookmarks", JSON.stringify(bookmarks));
334+
335+
render(
336+
<BrowserRouter>
337+
<Bookmarks />
338+
</BrowserRouter>,
339+
);
340+
341+
await waitFor(() => {
342+
expect(
343+
screen.getByRole("button", { name: /export bookmarks to csv/i }),
344+
).toBeInTheDocument();
345+
});
346+
});
347+
});
307348
});

src/types/bookmark.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Bookmark type definition for frequency management
3+
*/
4+
export interface Bookmark {
5+
id: string;
6+
frequency: number; // Hz
7+
name: string;
8+
tags: string[];
9+
notes: string;
10+
createdAt: number; // timestamp
11+
lastUsed: number; // timestamp
12+
}

0 commit comments

Comments
 (0)