diff --git a/GEMINI.md b/GEMINI.md index 6df5396..e4d3fde 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,3 +1,146 @@ ## Gemini Added Memories -- ./backend contains the FastAPI backend. It uses uv as package manager. To run backend tests, use uv run pytest -- ./frontend contains the TS React frontend. It uses ShadCN components. To run tests, run npm test +- ./backend contains the FastAPI backend. It uses uv as package manager. +- ./frontend contains the TS React frontend. It uses ShadCN components. + +## Useful commands: + +Run `make` with these targets: + install Install all dependencies for frontend and backend + dev Start the development servers for frontend and backend + test Run tests for frontend and backend + lint Lint the frontend and backend code + docker-build Build the production Docker images + docker-up Start the production application with Docker Compose + docker-down Stop the production application + docker-dev-up Start the development environment with Docker Compose + docker-dev-down Stop the development environment + clean Remove generated files and caches + +## Writing Tests + +### General Guidance + +- When adding tests, first examine existing tests to understand and conform to established conventions. +- Pay close attention to the mocks at the top of existing test files; they reveal critical dependencies and how they are managed in a test environment. + +## Git Repo + +The main branch for this project is called "main" + +## JavaScript/TypeScript + +When contributing to this React, Node, and TypeScript codebase, please prioritize the use of plain JavaScript objects with accompanying TypeScript interface or type declarations over JavaScript class syntax. This approach offers significant advantages, especially concerning interoperability with React and overall code maintainability. + +### Preferring Plain Objects over Classes + +JavaScript classes, by their nature, are designed to encapsulate internal state and behavior. While this can be useful in some object-oriented paradigms, it often introduces unnecessary complexity and friction when working with React's component-based architecture. Here's why plain objects are preferred: + +- Seamless React Integration: React components thrive on explicit props and state management. Classes' tendency to store internal state directly within instances can make prop and state propagation harder to reason about and maintain. Plain objects, on the other hand, are inherently immutable (when used thoughtfully) and can be easily passed as props, simplifying data flow and reducing unexpected side effects. + +- Reduced Boilerplate and Increased Conciseness: Classes often promote the use of constructors, this binding, getters, setters, and other boilerplate that can unnecessarily bloat code. TypeScript interface and type declarations provide powerful static type checking without the runtime overhead or verbosity of class definitions. This allows for more succinct and readable code, aligning with JavaScript's strengths in functional programming. + +- Enhanced Readability and Predictability: Plain objects, especially when their structure is clearly defined by TypeScript interfaces, are often easier to read and understand. Their properties are directly accessible, and there's no hidden internal state or complex inheritance chains to navigate. This predictability leads to fewer bugs and a more maintainable codebase. + +- Simplified Immutability: While not strictly enforced, plain objects encourage an immutable approach to data. When you need to modify an object, you typically create a new one with the desired changes, rather than mutating the original. This pattern aligns perfectly with React's reconciliation process and helps prevent subtle bugs related to shared mutable state. + +- Better Serialization and Deserialization: Plain JavaScript objects are naturally easy to serialize to JSON and deserialize back, which is a common requirement in web development (e.g., for API communication or local storage). Classes, with their methods and prototypes, can complicate this process. + +### Embracing ES Module Syntax for Encapsulation + +Rather than relying on Java-esque private or public class members, which can be verbose and sometimes limit flexibility, we strongly prefer leveraging ES module syntax (`import`/`export`) for encapsulating private and public APIs. + +- Clearer Public API Definition: With ES modules, anything that is exported is part of the public API of that module, while anything not exported is inherently private to that module. This provides a very clear and explicit way to define what parts of your code are meant to be consumed by other modules. + +- Enhanced Testability (Without Exposing Internals): By default, unexported functions or variables are not accessible from outside the module. This encourages you to test the public API of your modules, rather than their internal implementation details. If you find yourself needing to spy on or stub an unexported function for testing purposes, it's often a "code smell" indicating that the function might be a good candidate for extraction into its own separate, testable module with a well-defined public API. This promotes a more robust and maintainable testing strategy. + +- Reduced Coupling: Explicitly defined module boundaries through import/export help reduce coupling between different parts of your codebase. This makes it easier to refactor, debug, and understand individual components in isolation. + +### Avoiding `any` Types and Type Assertions; Preferring `unknown` + +TypeScript's power lies in its ability to provide static type checking, catching potential errors before your code runs. To fully leverage this, it's crucial to avoid the `any` type and be judicious with type assertions. + +- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type checking for that particular variable or expression. While it might seem convenient in the short term, it introduces significant risks: + - **Loss of Type Safety**: You lose all the benefits of type checking, making it easy to introduce runtime errors that TypeScript would otherwise have caught. + - **Reduced Readability and Maintainability**: Code with `any` types is harder to understand and maintain, as the expected type of data is no longer explicitly defined. + - **Masking Underlying Issues**: Often, the need for any indicates a deeper problem in the design of your code or the way you're interacting with external libraries. It's a sign that you might need to refine your types or refactor your code. + +- **Preferring `unknown` over `any`**: When you absolutely cannot determine the type of a value at compile time, and you're tempted to reach for any, consider using unknown instead. unknown is a type-safe counterpart to any. While a variable of type unknown can hold any value, you must perform type narrowing (e.g., using typeof or instanceof checks, or a type assertion) before you can perform any operations on it. This forces you to handle the unknown type explicitly, preventing accidental runtime errors. + + ``` + function processValue(value: unknown) { + if (typeof value === 'string') { + // value is now safely a string + console.log(value.toUpperCase()); + } else if (typeof value === 'number') { + // value is now safely a number + console.log(value * 2); + } + // Without narrowing, you cannot access properties or methods on 'value' + // console.log(value.someProperty); // Error: Object is of type 'unknown'. + } + ``` + +- **Type Assertions (`as Type`) - Use with Caution**: Type assertions tell the TypeScript compiler, "Trust me, I know what I'm doing; this is definitely of this type." While there are legitimate use cases (e.g., when dealing with external libraries that don't have perfect type definitions, or when you have more information than the compiler), they should be used sparingly and with extreme caution. + - **Bypassing Type Checking**: Like `any`, type assertions bypass TypeScript's safety checks. If your assertion is incorrect, you introduce a runtime error that TypeScript would not have warned you about. + - **Code Smell in Testing**: A common scenario where `any` or type assertions might be tempting is when trying to test "private" implementation details (e.g., spying on or stubbing an unexported function within a module). This is a strong indication of a "code smell" in your testing strategy and potentially your code structure. Instead of trying to force access to private internals, consider whether those internal details should be refactored into a separate module with a well-defined public API. This makes them inherently testable without compromising encapsulation. + +### Embracing JavaScript's Array Operators + +To further enhance code cleanliness and promote safe functional programming practices, leverage JavaScript's rich set of array operators as much as possible. Methods like `.map()`, `.filter()`, `.reduce()`, `.slice()`, `.sort()`, and others are incredibly powerful for transforming and manipulating data collections in an immutable and declarative way. + +Using these operators: + +- Promotes Immutability: Most array operators return new arrays, leaving the original array untouched. This functional approach helps prevent unintended side effects and makes your code more predictable. +- Improves Readability: Chaining array operators often lead to more concise and expressive code than traditional for loops or imperative logic. The intent of the operation is clear at a glance. +- Facilitates Functional Programming: These operators are cornerstones of functional programming, encouraging the creation of pure functions that take inputs and produce outputs without causing side effects. This paradigm is highly beneficial for writing robust and testable code that pairs well with React. + +By consistently applying these principles, we can maintain a codebase that is not only efficient and performant but also a joy to work with, both now and in the future. + +## React Guidelines + +### Follow these guidelines in all code you produce and suggest + +Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic. + +Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state. + +Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables. + +Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state. + +Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function. + +Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context. + +Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly. + +Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code. + +Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side effects. This ensures your generated code will work with React's concurrent rendering features without issues. + +Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests. + +Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable. + +Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance. + +### Process + +1. Analyze the user's code for optimization opportunities: + - Check for React anti-patterns that prevent compiler optimization + - Look for component structure issues that limit compiler effectiveness + - Think about each suggestion you are making and consult React docs for best practices + +2. Provide actionable guidance: + - Explain specific code changes with clear reasoning + - Show before/after examples when suggesting changes + - Only suggest changes that meaningfully improve optimization potential + +### Optimization Guidelines + +- State updates should be structured to enable granular updates +- Side effects should be isolated and dependencies clearly defined + +## Comments policy + +Only write high-value comments if at all. Avoid talking to the user through comments. diff --git a/backend/app/crud/newsletters.py b/backend/app/crud/newsletters.py index 684d3c0..54558fb 100644 --- a/backend/app/crud/newsletters.py +++ b/backend/app/crud/newsletters.py @@ -51,7 +51,9 @@ def create_newsletter(db: Session, newsletter: NewsletterCreate): """Create a new newsletter.""" logger.info(f"Creating new newsletter with name '{newsletter.name}'") db_newsletter = Newsletter( - name=newsletter.name, extract_content=newsletter.extract_content + name=newsletter.name, + extract_content=newsletter.extract_content, + move_to_folder=newsletter.move_to_folder, ) db.add(db_newsletter) db.commit() @@ -79,6 +81,8 @@ def update_newsletter( return None db_newsletter.name = newsletter_update.name + db_newsletter.move_to_folder = newsletter_update.move_to_folder + db_newsletter.extract_content = newsletter_update.extract_content # Simple approach: delete existing senders and add new ones for sender in db_newsletter.senders: diff --git a/backend/app/models/newsletters.py b/backend/app/models/newsletters.py index d29244b..c9c68d2 100644 --- a/backend/app/models/newsletters.py +++ b/backend/app/models/newsletters.py @@ -11,6 +11,7 @@ class Newsletter(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String) + move_to_folder = Column(String, nullable=True) is_active = Column(Boolean, default=True) extract_content = Column(Boolean, default=False) diff --git a/backend/app/schemas/newsletters.py b/backend/app/schemas/newsletters.py index c7c1b74..25f4d2d 100644 --- a/backend/app/schemas/newsletters.py +++ b/backend/app/schemas/newsletters.py @@ -28,6 +28,7 @@ class NewsletterBase(BaseModel): """Base schema for a newsletter.""" name: str + move_to_folder: str | None = None extract_content: bool = False diff --git a/backend/app/services/email_processor.py b/backend/app/services/email_processor.py index e5f555d..63056f7 100644 --- a/backend/app/services/email_processor.py +++ b/backend/app/services/email_processor.py @@ -146,9 +146,10 @@ def _process_single_email( logger.debug(f"Marking email with id={num} as read") mail.store(num, "+FLAGS", "\\Seen") - if settings.move_to_folder: - logger.debug(f"Moving email with id={num} to {settings.move_to_folder}") - mail.copy(num, settings.move_to_folder) + move_folder = newsletter.move_to_folder or settings.move_to_folder + if move_folder: + logger.debug(f"Moving email with id={num} to {move_folder}") + mail.copy(num, move_folder) mail.store(num, "+FLAGS", "\\Deleted") diff --git a/backend/app/tests/test_crud.py b/backend/app/tests/test_crud.py index 90f00e9..9290cda 100644 --- a/backend/app/tests/test_crud.py +++ b/backend/app/tests/test_crud.py @@ -110,6 +110,23 @@ def test_create_newsletter(db_session: Session): assert newsletter.senders[0].email == unique_email +def test_create_newsletter_with_move_to_folder(db_session: Session): + """Test creating a newsletter with the move_to_folder attribute.""" + unique_email = f"sender_{uuid.uuid4()}@test.com" + newsletter_data = NewsletterCreate( + name="Test Newsletter with Folder", + sender_emails=[unique_email], + move_to_folder="Archive/Test", + extract_content=True, + ) + newsletter = create_newsletter(db_session, newsletter_data) + retrieved_newsletter = get_newsletter(db_session, newsletter.id) + + assert retrieved_newsletter.name == "Test Newsletter with Folder" + assert retrieved_newsletter.move_to_folder == "Archive/Test" + assert retrieved_newsletter.extract_content is True + + def test_get_newsletter(db_session: Session): """Test getting a single newsletter.""" unique_email = f"sender_{uuid.uuid4()}@test.com" diff --git a/backend/app/tests/test_email_processor.py b/backend/app/tests/test_email_processor.py new file mode 100644 index 0000000..7674fca --- /dev/null +++ b/backend/app/tests/test_email_processor.py @@ -0,0 +1,97 @@ +import imaplib +from email.message import Message +from unittest.mock import MagicMock + +from sqlalchemy.orm import Session + +from app.crud.newsletters import create_newsletter +from app.crud.settings import create_or_update_settings +from app.schemas.newsletters import NewsletterCreate +from app.schemas.settings import SettingsCreate +from app.services.email_processor import _process_single_email + + +def test_process_single_email_with_newsletter_move_folder(db_session: Session): + """Test that the per-newsletter move_to_folder is used when set, overriding the global setting.""" + # 1. ARRANGE + # Global settings with a move folder + settings_data = SettingsCreate( + imap_server="test.com", + imap_username="test", + imap_password="password", + move_to_folder="GlobalArchive", + ) + settings = create_or_update_settings(db_session, settings_data) + + # Newsletter with a specific move folder + newsletter_data = NewsletterCreate( + name="Test Newsletter", + sender_emails=["test@example.com"], + ) + newsletter = create_newsletter(db_session, newsletter_data) + newsletter.move_to_folder = "NewsletterArchive" + db_session.commit() + + # Mock IMAP mail object + mock_mail = MagicMock(spec=imaplib.IMAP4_SSL) + + # Mock email message + msg = Message() + msg["From"] = "test@example.com" + msg["Subject"] = "Test Email" + msg["Message-ID"] = "" + + # Mock mail.fetch to return the message + mock_mail.fetch.return_value = ("OK", [(b"1 (RFC822)", msg.as_bytes())]) + + sender_map = {"test@example.com": newsletter} + + # 2. ACT + _process_single_email("1", mock_mail, db_session, sender_map, settings) + + # 3. ASSERT + # Check that the email was moved to the newsletter-specific folder + mock_mail.copy.assert_called_once_with("1", "NewsletterArchive") + mock_mail.store.assert_any_call("1", "+FLAGS", "\\Deleted") + + +def test_process_single_email_with_global_move_folder(db_session: Session): + """Test that the global move_to_folder is used when the per-newsletter setting is not set.""" + # 1. ARRANGE + # Global settings with a move folder + settings_data = SettingsCreate( + imap_server="test.com", + imap_username="test", + imap_password="password", + move_to_folder="GlobalArchive", + ) + settings = create_or_update_settings(db_session, settings_data) + + # Newsletter without a specific move folder + newsletter_data = NewsletterCreate( + name="Test Newsletter", + sender_emails=["test@example.com"], + ) + newsletter = create_newsletter(db_session, newsletter_data) + + # Mock IMAP mail object + mock_mail = MagicMock(spec=imaplib.IMAP4_SSL) + + # Mock email message + msg = Message() + msg["From"] = "test@example.com" + msg["Subject"] = "Test Email" + msg["Message-ID"] = "" + + # Mock mail.fetch to return the message + mock_mail.fetch.return_value = ("OK", [(b"1 (RFC822)", msg.as_bytes())]) + + sender_map = {"test@example.com": newsletter} + + # 2. ACT + _process_single_email("1", mock_mail, db_session, sender_map, settings) + + # 3. ASSERT + # Check that the email was moved to the global folder + mock_mail.copy.assert_called_once_with("1", "GlobalArchive") + mock_mail.store.assert_any_call("1", "+FLAGS", "\\Deleted") diff --git a/frontend/components/letterfeed/AddNewsletterDialog.tsx b/frontend/components/letterfeed/AddNewsletterDialog.tsx index 0cc1ca8..f04cda0 100644 --- a/frontend/components/letterfeed/AddNewsletterDialog.tsx +++ b/frontend/components/letterfeed/AddNewsletterDialog.tsx @@ -14,16 +14,26 @@ import { Plus } from "lucide-react" import { createNewsletter } from "@/lib/api" import { Checkbox } from "@/components/ui/checkbox" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + interface AddNewsletterDialogProps { isOpen: boolean + folderOptions: string[] onOpenChange: (isOpen: boolean) => void onSuccess: () => void } -export function AddNewsletterDialog({ isOpen, onOpenChange, onSuccess }: AddNewsletterDialogProps) { +export function AddNewsletterDialog({ isOpen, folderOptions, onOpenChange, onSuccess }: AddNewsletterDialogProps) { const [newNewsletter, setNewNewsletter] = useState({ name: "", emails: [""], + move_to_folder: "", extract_content: false, }) @@ -54,9 +64,10 @@ export function AddNewsletterDialog({ isOpen, onOpenChange, onSuccess }: AddNews await createNewsletter({ name: newNewsletter.name, sender_emails: newNewsletter.emails.filter((email) => email.trim()), + move_to_folder: newNewsletter.move_to_folder, extract_content: newNewsletter.extract_content, }) - setNewNewsletter({ name: "", emails: [""], extract_content: false }) + setNewNewsletter({ name: "", emails: [""], move_to_folder: "", extract_content: false }) onOpenChange(false) onSuccess() } catch (error) { @@ -83,6 +94,28 @@ export function AddNewsletterDialog({ isOpen, onOpenChange, onSuccess }: AddNews /> +
+ + +
+
{newNewsletter.emails.map((email, index) => ( diff --git a/frontend/components/letterfeed/EditNewsletterDialog.tsx b/frontend/components/letterfeed/EditNewsletterDialog.tsx index 4677d94..50b915a 100644 --- a/frontend/components/letterfeed/EditNewsletterDialog.tsx +++ b/frontend/components/letterfeed/EditNewsletterDialog.tsx @@ -14,17 +14,27 @@ 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, onOpenChange, onSuccess }: EditNewsletterDialogProps) { - const [editedDetails, setEditedDetails] = useState<{ name: string; emails: string[], extract_content: boolean }>({ +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, }) @@ -33,6 +43,7 @@ export function EditNewsletterDialog({ newsletter, isOpen, onOpenChange, onSucce setEditedDetails({ name: newsletter.name, emails: newsletter.senders.map((s) => s.email), + move_to_folder: newsletter.move_to_folder || "", extract_content: newsletter.extract_content, }) } @@ -66,6 +77,7 @@ export function EditNewsletterDialog({ newsletter, isOpen, onOpenChange, onSucce 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) @@ -103,6 +115,27 @@ export function EditNewsletterDialog({ newsletter, isOpen, onOpenChange, onSucce onChange={(e) => setEditedDetails((prev) => ({ ...prev, name: e.target.value }))} />
+
+ + +
{editedDetails.emails.map((email, index) => ( diff --git a/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx b/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx index c72d6d4..bd3ef7f 100644 --- a/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx +++ b/frontend/components/letterfeed/__tests__/AddNewsletterDialog.test.tsx @@ -30,7 +30,7 @@ describe("AddNewsletterDialog", () => { entries_count: 0, }) - render() + render() // Fill out the form fireEvent.change(screen.getByLabelText(/Newsletter Name/i), { target: { value: "My New Newsletter" } }) @@ -44,6 +44,7 @@ describe("AddNewsletterDialog", () => { expect(mockedApi.createNewsletter).toHaveBeenCalledWith({ name: "My New Newsletter", sender_emails: ["test@example.com"], + move_to_folder: "", extract_content: false, }) expect(handleSuccess).toHaveBeenCalledTimes(1) @@ -52,7 +53,7 @@ describe("AddNewsletterDialog", () => { }) it("allows adding and removing email fields", () => { - render( {}} onSuccess={() => {}} />) + render( {}} onSuccess={() => {}} />) // Initial state expect(screen.getAllByPlaceholderText(/Enter email address/i)).toHaveLength(1) @@ -67,7 +68,7 @@ describe("AddNewsletterDialog", () => { }) it("closes the dialog when cancel is clicked", () => { - render() + render() fireEvent.click(screen.getByRole("button", { name: /Cancel/i })) expect(handleOpenChange).toHaveBeenCalledWith(false) diff --git a/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx b/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx index 16abc65..36cbb6c 100644 --- a/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx +++ b/frontend/components/letterfeed/__tests__/EditNewsletterDialog.test.tsx @@ -18,6 +18,7 @@ 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, } @@ -39,6 +40,7 @@ describe("EditNewsletterDialog", () => { @@ -59,6 +61,8 @@ describe("EditNewsletterDialog", () => { 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) @@ -72,6 +76,7 @@ describe("EditNewsletterDialog", () => { @@ -95,6 +100,7 @@ describe("EditNewsletterDialog", () => { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ed348f7..aff95ba 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -73,6 +73,7 @@ export default function LetterFeedApp() { @@ -81,6 +82,7 @@ export default function LetterFeedApp() { { setEditingNewsletter(null) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index fde6406..c205fc6 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -10,6 +10,7 @@ export interface Newsletter { id: number name: string is_active: boolean + move_to_folder?: string | null extract_content: boolean senders: { id: number; email: string }[] entries_count: number @@ -18,12 +19,14 @@ export interface Newsletter { export interface NewsletterCreate { name: string; sender_emails: string[]; + move_to_folder?: string | null; extract_content: boolean; } export interface NewsletterUpdate { name: string; sender_emails: string[]; + move_to_folder?: string | null; extract_content: boolean; }