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
1 change: 1 addition & 0 deletions frontend/src/features/readers/reader-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function ReaderSelector() {
return (
<Link
to="/readers"
state={{ openCreateReader: true }}
className="flex h-11 items-center gap-2 rounded-xl border border-[#d7d5c9] bg-white px-3 text-sm font-semibold text-[#31564c] hover:bg-[#f9f7f1]"
>
<UserPlus className="size-4" aria-hidden="true" />
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/routes/readers-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
import { MemoryRouter } from "react-router-dom";

import { ApiError } from "@/api/client";
import type { Reader } from "@/features/readers/reader-api";
Expand Down Expand Up @@ -56,6 +57,21 @@ describe("ReadersPage", () => {
);
});

it("opens the create form when requested by the reader selector", async () => {
apiMocks.fetch.mockImplementation((path: string) => {
if (path === "/readers") return Promise.resolve([]);
if (path === "/reader-login-invitations") return Promise.resolve([]);
throw new Error(`Unexpected request: ${path}`);
});

renderReadersPage({ openCreateReader: true });

expect(
await screen.findByRole("heading", { name: "Add a reader" }),
).toBeVisible();
expect(screen.getByLabelText("Name")).toBeVisible();
});

it("sends a reader invitation using only an email address", async () => {
const user = userEvent.setup();
apiMocks.fetch.mockImplementation((path: string, init?: RequestInit) => {
Expand Down Expand Up @@ -188,13 +204,15 @@ describe("ReadersPage", () => {
});
});

function renderReadersPage() {
function renderReadersPage(state?: { openCreateReader: boolean }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
const Wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ReaderSelectionProvider>{children}</ReaderSelectionProvider>
<MemoryRouter initialEntries={[{ pathname: "/readers", state }]}>
<ReaderSelectionProvider>{children}</ReaderSelectionProvider>
</MemoryRouter>
</QueryClientProvider>
);
return render(<ReadersPage />, { wrapper: Wrapper });
Expand Down
25 changes: 20 additions & 5 deletions frontend/src/routes/readers-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "lucide-react";
import { useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { useLocation, useNavigate } from "react-router-dom";
import { z } from "zod";

import { ApiError } from "@/api/client";
Expand Down Expand Up @@ -51,6 +52,8 @@ const readerFormSchema = z.object({
type ReaderFormValues = z.infer<typeof readerFormSchema>;

export function ReadersPage() {
const location = useLocation();
const navigate = useNavigate();
const { data: readers = [], isLoading, error } = useReaders();
const createMutation = useCreateReader();
const invitations = useReaderLoginInvitations();
Expand All @@ -63,11 +66,23 @@ export function ReadersPage() {
const [deletingReader, setDeletingReader] = useState<Reader | null>(null);
const [accessToRemove, setAccessToRemove] =
useState<ReaderLoginInvitation | null>(null);
const navigationState = location.state as {
openCreateReader?: boolean;
} | null;
const createFormRequested = Boolean(navigationState?.openCreateReader);
const isCreateFormOpen = showCreateForm || createFormRequested;

function closeCreateForm() {
setShowCreateForm(false);
if (createFormRequested) {
void navigate(location.pathname, { replace: true, state: null });
}
}

async function handleCreate(data: ReaderCreate) {
const reader = await createMutation.mutateAsync(data);
if (!selectedReaderId) setSelectedReaderId(reader.id);
setShowCreateForm(false);
closeCreateForm();
}

async function handleUpdate(data: ReaderCreate) {
Expand Down Expand Up @@ -232,7 +247,7 @@ export function ReadersPage() {
)}
</Card>

{showCreateForm ? (
{isCreateFormOpen ? (
<Card className="mb-6 p-5 sm:p-7">
<div className="mb-5 flex items-start justify-between gap-4">
<div>
Expand All @@ -247,7 +262,7 @@ export function ReadersPage() {
variant="ghost"
size="icon"
aria-label="Close new reader form"
onClick={() => setShowCreateForm(false)}
onClick={closeCreateForm}
>
<X className="size-5" />
</Button>
Expand All @@ -257,7 +272,7 @@ export function ReadersPage() {
isPending={createMutation.isPending}
error={createMutation.error}
onSubmit={handleCreate}
onCancel={() => setShowCreateForm(false)}
onCancel={closeCreateForm}
/>
</Card>
) : null}
Expand All @@ -270,7 +285,7 @@ export function ReadersPage() {
</Card>
) : null}

{!isLoading && !error && readers.length === 0 && !showCreateForm ? (
{!isLoading && !error && readers.length === 0 && !isCreateFormOpen ? (
<Card className="flex flex-col items-center px-6 py-14 text-center">
<span className="mb-5 flex size-16 items-center justify-center rounded-full bg-[#e8efe9] text-[#35675a]">
<UsersRound className="size-8" />
Expand Down