diff --git a/frontend/components/letterfeed/EditNewsletterDialog.tsx b/frontend/components/letterfeed/EditNewsletterDialog.tsx deleted file mode 100644 index 50b915a..0000000 --- a/frontend/components/letterfeed/EditNewsletterDialog.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { useState, useEffect } from "react" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Plus } from "lucide-react" -import { Newsletter, updateNewsletter, deleteNewsletter } from "@/lib/api" -import { Checkbox } from "@/components/ui/checkbox" - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -interface EditNewsletterDialogProps { - newsletter: Newsletter | null - isOpen: boolean - folderOptions: string[] - onOpenChange: (isOpen: boolean) => void - onSuccess: () => void -} - -export function EditNewsletterDialog({ newsletter, isOpen, folderOptions, onOpenChange, onSuccess }: EditNewsletterDialogProps) { - const [editedDetails, setEditedDetails] = useState<{ name: string; emails: string[], move_to_folder: string | null, extract_content: boolean }>({ - name: "", - emails: [], - move_to_folder: "", - extract_content: false, - }) - - useEffect(() => { - if (newsletter) { - setEditedDetails({ - name: newsletter.name, - emails: newsletter.senders.map((s) => s.email), - move_to_folder: newsletter.move_to_folder || "", - extract_content: newsletter.extract_content, - }) - } - }, [newsletter]) - - if (!newsletter) return null - - const handleUpdateEmailChange = (index: number, value: string) => { - setEditedDetails((prev) => ({ - ...prev, - emails: prev.emails.map((email, i) => (i === index ? value : email)), - })) - } - - const handleAddEmailToEdit = () => { - setEditedDetails((prev) => ({ - ...prev, - emails: [...prev.emails, ""], - })) - } - - const handleRemoveEmailFromEdit = (index: number) => { - setEditedDetails((prev) => ({ - ...prev, - emails: prev.emails.filter((_, i) => i !== index), - })) - } - - const handleUpdate = async () => { - try { - await updateNewsletter(newsletter.id, { - name: editedDetails.name, - sender_emails: editedDetails.emails.filter((email) => email.trim()), - move_to_folder: editedDetails.move_to_folder, - extract_content: editedDetails.extract_content, - }) - onOpenChange(false) - onSuccess() - } catch (error) { - console.error("Failed to update newsletter:", error) - } - } - - const handleDelete = async () => { - if (window.confirm(`Are you sure you want to delete the "${newsletter.name}" newsletter?`)) { - try { - await deleteNewsletter(newsletter.id) - onOpenChange(false) - onSuccess() - } catch (error) { - console.error("Failed to delete newsletter:", error) - } - } - } - - return ( - - - - Edit Newsletter - Update the details for {newsletter.name}. - -
-
- - setEditedDetails((prev) => ({ ...prev, name: e.target.value }))} - /> -
-
- - -
-
- - {editedDetails.emails.map((email, index) => ( -
- handleUpdateEmailChange(index, e.target.value)} - placeholder="Enter email address" - type="email" - /> - {editedDetails.emails.length > 1 && ( - - )} -
- ))} - -
-
- - setEditedDetails((prev) => ({ ...prev, extract_content: !!checked })) - } - /> - -
-
- - -
- - -
-
-
-
- ) -} diff --git a/frontend/components/letterfeed/NewsletterCard.tsx b/frontend/components/letterfeed/NewsletterCard.tsx index 96df60f..aee6e68 100644 --- a/frontend/components/letterfeed/NewsletterCard.tsx +++ b/frontend/components/letterfeed/NewsletterCard.tsx @@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Rss, Mail, ExternalLink, Edit } from "lucide-react" import { Newsletter, getFeedUrl } from "@/lib/api" -import { useEffect, useState } from "react" interface NewsletterCardProps { newsletter: Newsletter @@ -11,16 +10,7 @@ interface NewsletterCardProps { } export function NewsletterCard({ newsletter, onEdit }: NewsletterCardProps) { - const [absoluteFeedUrl, setAbsoluteFeedUrl] = useState(getFeedUrl(newsletter.id)) - - useEffect(() => { - const url = getFeedUrl(newsletter.id) - if (url.startsWith("/")) { - setAbsoluteFeedUrl(`${window.location.origin}${url}`) - } else { - setAbsoluteFeedUrl(url) - } - }, [newsletter.id]) + const feedUrl = getFeedUrl(newsletter.id) return ( @@ -32,8 +22,8 @@ export function NewsletterCard({ newsletter, onEdit }: NewsletterCardProps) { {newsletter.name} - {newsletter.entries_count} entr{newsletter.entries_count !== 1 ? "ies" : "y"} - + {newsletter.entries_count} entr{newsletter.entries_count !== 1 ? "ies" : "y"} + @@ -141,19 +181,28 @@ export function AddNewsletterDialog({ isOpen, folderOptions, onOpenChange, onSuc
- setNewNewsletter((prev) => ({ ...prev, extract_content: !!checked })) + setFormData((prev) => ({ ...prev, extract_content: !!checked })) } />
- - - + + {isEditMode && ( + + )} +
+ + +
diff --git a/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx b/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx deleted file mode 100644 index bd3ef7f..0000000 --- a/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from "react" -import { render, screen, fireEvent, waitFor } from "@testing-library/react" -import "@testing-library/jest-dom" -import { AddNewsletterDialog } from "../AddNewsletterDialog" -import * as api from "@/lib/api" - -// Mock the API module -jest.mock("@/lib/api", () => ({ - ...jest.requireActual("@/lib/api"), - createNewsletter: jest.fn(), -})) - -const mockedApi = api as jest.Mocked - -describe("AddNewsletterDialog", () => { - const handleOpenChange = jest.fn() - const handleSuccess = jest.fn() - - beforeEach(() => { - jest.clearAllMocks() - }) - - it("allows user to fill out the form and submit", async () => { - mockedApi.createNewsletter.mockResolvedValueOnce({ - id: 1, - name: "My New Newsletter", - is_active: true, - extract_content: false, - senders: [{ id: 1, email: "test@example.com", newsletter_id: 1 }], - entries_count: 0, - }) - - render() - - // Fill out the form - fireEvent.change(screen.getByLabelText(/Newsletter Name/i), { target: { value: "My New Newsletter" } }) - fireEvent.change(screen.getByPlaceholderText(/Enter email address/i), { target: { value: "test@example.com" } }) - - // Submit the form - fireEvent.click(screen.getByRole("button", { name: /Register Newsletter/i })) - - // Wait for the async operation to complete - await waitFor(() => { - expect(mockedApi.createNewsletter).toHaveBeenCalledWith({ - name: "My New Newsletter", - sender_emails: ["test@example.com"], - move_to_folder: "", - extract_content: false, - }) - expect(handleSuccess).toHaveBeenCalledTimes(1) - expect(handleOpenChange).toHaveBeenCalledWith(false) - }) - }) - - it("allows adding and removing email fields", () => { - render( {}} onSuccess={() => {}} />) - - // Initial state - expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1) - - // Add another email - fireEvent.click(screen.getByRole("button", { name: /Add Another Email/i })) - expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(2) - - // Remove the first email - fireEvent.click(screen.getAllByRole("button", { name: /Remove/i })[0]) - expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1) - }) - - it("closes the dialog when cancel is clicked", () => { - render() - - fireEvent.click(screen.getByRole("button", { name: /Cancel/i })) - expect(handleOpenChange).toHaveBeenCalledWith(false) - expect(handleSuccess).not.toHaveBeenCalled() - }) -}) diff --git a/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx b/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx deleted file mode 100644 index 36cbb6c..0000000 --- a/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from "react" -import { render, screen, fireEvent, waitFor } from "@testing-library/react" -import "@testing-library/jest-dom" -import { EditNewsletterDialog } from "../EditNewsletterDialog" -import { Newsletter } from "@/lib/api" -import * as api from "@/lib/api" - -// Mock the API module -jest.mock("@/lib/api", () => ({ - ...jest.requireActual("@/lib/api"), - updateNewsletter: jest.fn(), - deleteNewsletter: jest.fn(), -})) - -const mockedApi = api as jest.Mocked - -const mockNewsletter: Newsletter = { - id: 1, - name: "Existing Newsletter", - is_active: true, - extract_content: false, - senders: [{ id: 1, email: "current@example.com", newsletter_id: 1 }], - entries_count: 5, -} - -describe("EditNewsletterDialog", () => { - const handleOpenChange = jest.fn() - const handleSuccess = jest.fn() - - beforeEach(() => { - jest.clearAllMocks() - // Mock window.confirm for the delete action - window.confirm = jest.fn(() => true) - }) - - it("renders with initial newsletter data and allows updates", async () => { - mockedApi.updateNewsletter.mockResolvedValueOnce({ ...mockNewsletter, name: "Updated Name" }) - - render( - - ) - - // Check that initial data is present - const nameInput = screen.getByLabelText(/Newsletter Name/i) - expect(nameInput).toHaveValue("Existing Newsletter") - expect(screen.getByDisplayValue("current@example.com")).toBeInTheDocument() - - // Update the name - fireEvent.change(nameInput, { target: { value: "Updated Name" } }) - - // Submit - fireEvent.click(screen.getByRole("button", { name: /Save Changes/i })) - - await waitFor(() => { - expect(mockedApi.updateNewsletter).toHaveBeenCalledWith(1, { - name: "Updated Name", - sender_emails: ["current@example.com"], - move_to_folder: "", - extract_content: false, - }) - expect(handleSuccess).toHaveBeenCalledTimes(1) - expect(handleOpenChange).toHaveBeenCalledWith(false) - }) - }) - - it("calls deleteNewsletter when delete button is clicked and confirmed", async () => { - mockedApi.deleteNewsletter.mockResolvedValueOnce() - - render( - - ) - - fireEvent.click(screen.getByRole("button", { name: /Delete Newsletter/i })) - - expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete the "Existing Newsletter" newsletter?') - - await waitFor(() => { - expect(mockedApi.deleteNewsletter).toHaveBeenCalledWith(1) - expect(handleSuccess).toHaveBeenCalledTimes(1) - expect(handleOpenChange).toHaveBeenCalledWith(false) - }) - }) - - it("does not call deleteNewsletter when delete is not confirmed", () => { - window.confirm = jest.fn(() => false) // User clicks "Cancel" - - render( - - ) - - fireEvent.click(screen.getByRole("button", { name: /Delete Newsletter/i })) - - expect(mockedApi.deleteNewsletter).not.toHaveBeenCalled() - expect(handleSuccess).not.toHaveBeenCalled() - }) -}) diff --git a/frontend/components/letterfeed/__tests__/NewsletterDialog.test.tsx b/frontend/components/letterfeed/__tests__/NewsletterDialog.test.tsx new file mode 100644 index 0000000..9e680af --- /dev/null +++ b/frontend/components/letterfeed/__tests__/NewsletterDialog.test.tsx @@ -0,0 +1,136 @@ +import React from "react" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import "@testing-library/jest-dom" +import { NewsletterDialog } from "../NewsletterDialog" +import { Newsletter } from "@/lib/api" +import * as api from "@/lib/api" + +// Mock the API module +jest.mock("@/lib/api", () => ({ + ...jest.requireActual("@/lib/api"), + createNewsletter: jest.fn(), + updateNewsletter: jest.fn(), + deleteNewsletter: jest.fn(), +})) + +const mockedApi = api as jest.Mocked + +const mockNewsletter: Newsletter = { + id: "1", + name: "Existing Newsletter", + is_active: true, + extract_content: false, + senders: [{ id: "1", email: "current@example.com" }], + entries_count: 5, + move_to_folder: "", +} + +describe("NewsletterDialog", () => { + const handleOpenChange = jest.fn() + const handleSuccess = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + window.confirm = jest.fn(() => true) + }) + + describe("Add Mode", () => { + it("allows user to fill out the form and submit", async () => { + mockedApi.createNewsletter.mockResolvedValueOnce({ + id: "2", + name: "My New Newsletter", + is_active: true, + extract_content: false, + senders: [{ id: "2", email: "test@example.com" }], + entries_count: 0, + }) + + render() + + expect(screen.getByText("Register New Newsletter")).toBeInTheDocument() + + fireEvent.change(screen.getByLabelText(/Newsletter Name/i), { target: { value: "My New Newsletter" } }) + fireEvent.change(screen.getByPlaceholderText(/Enter email address/i), { target: { value: "test@example.com" } }) + + fireEvent.click(screen.getByRole("button", { name: /Register Newsletter/i })) + + await waitFor(() => { + expect(mockedApi.createNewsletter).toHaveBeenCalledWith({ + name: "My New Newsletter", + sender_emails: ["test@example.com"], + move_to_folder: "", + extract_content: false, + }) + expect(handleSuccess).toHaveBeenCalledTimes(1) + expect(handleOpenChange).toHaveBeenCalledWith(false) + }) + }) + + it("allows adding and removing email fields", () => { + render( {}} onSuccess={() => {}} />) + expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1) + fireEvent.click(screen.getByRole("button", { name: /Add Another Email/i })) + expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(2) + fireEvent.click(screen.getAllByRole("button", { name: /Remove/i })[0]) + expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1) + }) + }) + + describe("Edit Mode", () => { + it("renders with initial newsletter data and allows updates", async () => { + mockedApi.updateNewsletter.mockResolvedValueOnce({ ...mockNewsletter, name: "Updated Name" }) + + render( + + ) + + expect(screen.getByText("Edit Newsletter")).toBeInTheDocument() + const nameInput = screen.getByLabelText(/Newsletter Name/i) + expect(nameInput).toHaveValue("Existing Newsletter") + expect(screen.getByDisplayValue("current@example.com")).toBeInTheDocument() + + fireEvent.change(nameInput, { target: { value: "Updated Name" } }) + fireEvent.click(screen.getByRole("button", { name: /Save Changes/i })) + + await waitFor(() => { + expect(mockedApi.updateNewsletter).toHaveBeenCalledWith("1", { + name: "Updated Name", + sender_emails: ["current@example.com"], + move_to_folder: "", + extract_content: false, + }) + expect(handleSuccess).toHaveBeenCalledTimes(1) + expect(handleOpenChange).toHaveBeenCalledWith(false) + }) + }) + + it("calls deleteNewsletter when delete button is clicked and confirmed", async () => { + mockedApi.deleteNewsletter.mockResolvedValueOnce(undefined) + + render( + + ) + + fireEvent.click(screen.getByRole("button", { name: /Delete Newsletter/i })) + expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete the "Existing Newsletter" newsletter?') + + await waitFor(() => { + expect(mockedApi.deleteNewsletter).toHaveBeenCalledWith("1") + expect(handleSuccess).toHaveBeenCalledTimes(1) + expect(handleOpenChange).toHaveBeenCalledWith(false) + }) + }) + }) +}) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index aff95ba..075dc9a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -12,8 +12,7 @@ import { LoadingSpinner } from "@/components/letterfeed/LoadingSpinner" import { Header } from "@/components/letterfeed/Header" import { NewsletterList } from "@/components/letterfeed/NewsletterList" import { EmptyState } from "@/components/letterfeed/EmptyState" -import { AddNewsletterDialog } from "@/components/letterfeed/AddNewsletterDialog" -import { EditNewsletterDialog } from "@/components/letterfeed/EditNewsletterDialog" +import { NewsletterDialog } from "@/components/letterfeed/NewsletterDialog" import { SettingsDialog } from "@/components/letterfeed/SettingsDialog" export default function LetterFeedApp() { @@ -24,7 +23,6 @@ export default function LetterFeedApp() { const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isSettingsOpen, setIsSettingsOpen] = useState(false) - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [editingNewsletter, setEditingNewsletter] = useState(null) const fetchData = useCallback(async () => { @@ -50,7 +48,10 @@ export default function LetterFeedApp() { const openEditDialog = (newsletter: Newsletter) => { setEditingNewsletter(newsletter) - setIsEditDialogOpen(true) + } + + const closeEditDialog = () => { + setEditingNewsletter(null) } if (isLoading) { @@ -71,25 +72,23 @@ export default function LetterFeedApp() { setIsAddDialogOpen(true)} /> )} - - {editingNewsletter && ( - { - setEditingNewsletter(null) - fetchData() - }} - /> - )} + { + closeEditDialog() + fetchData() + }} + /> {settings && (