diff --git a/webui/src/routes/issues/-route.test.tsx b/webui/src/routes/issues/-route.test.tsx index 7a0abdcd..55492773 100644 --- a/webui/src/routes/issues/-route.test.tsx +++ b/webui/src/routes/issues/-route.test.tsx @@ -136,7 +136,11 @@ describe('issues route', () => { it('renders stats and list items through the app router', async () => { renderIssuesRoute(); await waitFor(() => expect(screen.getByTestId('issue-counts')).toHaveTextContent('2')); - expect(await screen.findByTestId('issue-card-7')).toHaveTextContent('Bad tags'); + const issueCard = await screen.findByRole('link', { name: /Bad tags/i }); + expect(issueCard).toHaveAttribute('href', expect.stringContaining('/issues?')); + expect(issueCard).toHaveAttribute('href', expect.stringContaining('status=open')); + expect(issueCard).toHaveAttribute('href', expect.stringContaining('category=all')); + expect(issueCard).toHaveAttribute('href', expect.stringContaining('issueId=7')); }); it('loads the detail modal from the route search state', async () => { @@ -157,17 +161,22 @@ describe('issues route', () => { it('opens and closes the detail modal', async () => { const { history } = renderIssuesRoute(); - fireEvent.click(await screen.findByTestId('issue-card-7')); + fireEvent.click(await screen.findByRole('link', { name: /Bad tags/i })); await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7')); await waitFor(() => expect(history.location.search).toContain('issueId=7')); - fireEvent.click(screen.getByRole('button', { name: /close issue detail/i })); + const closeLink = screen.getByRole('link', { name: /^close$/i }); + expect(closeLink).toHaveAttribute( + 'href', + expect.stringContaining('/issues?status=open&category=all'), + ); + fireEvent.click(closeLink); await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); await waitFor(() => expect(history.location.search).toBe('?status=open&category=all')); }); it('closes the detail modal with Escape', async () => { const { history } = renderIssuesRoute(); - fireEvent.click(await screen.findByTestId('issue-card-7')); + fireEvent.click(await screen.findByRole('link', { name: /Bad tags/i })); await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7')); await waitFor(() => expect(history.location.search).toContain('issueId=7')); @@ -179,7 +188,7 @@ describe('issues route', () => { it('focuses the detail modal close button on open', async () => { renderIssuesRoute(); - fireEvent.click(await screen.findByTestId('issue-card-7')); + fireEvent.click(await screen.findByRole('link', { name: /Bad tags/i })); const closeButton = await screen.findByRole('button', { name: /close issue detail/i, @@ -190,7 +199,7 @@ describe('issues route', () => { it('invokes the shared workflow adapter for admin downloads', async () => { renderIssuesRoute(); - fireEvent.click(await screen.findByTestId('issue-card-7')); + fireEvent.click(await screen.findByRole('link', { name: /Bad tags/i })); fireEvent.click(await screen.findByRole('button', { name: /download album/i })); await waitFor(() => expect(workflowActions.openDownloadMissingAlbum).toHaveBeenCalled()); expect(workflowActions.openDownloadMissingAlbum).toHaveBeenCalledWith( diff --git a/webui/src/routes/issues/-ui/issue-detail-modal.module.css b/webui/src/routes/issues/-ui/issue-detail-modal.module.css index 7348fa43..48a25c54 100644 --- a/webui/src/routes/issues/-ui/issue-detail-modal.module.css +++ b/webui/src/routes/issues/-ui/issue-detail-modal.module.css @@ -878,6 +878,43 @@ font-size: 13px; } +.modalLinkButton { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 600; + line-height: 1.2; + min-height: 36px; + padding: 8px 12px; + text-decoration: none; + transition: + transform 0.18s ease, + border-color 0.18s ease, + box-shadow 0.18s ease, + background 0.18s ease, + color 0.18s ease; +} + +.modalLinkButton:hover { + transform: translateY(-1px); + border-color: rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.08); +} + +.modalLinkButton:focus-visible { + outline: none; + border-color: rgba(var(--accent-light-rgb), 0.55); + box-shadow: 0 0 0 3px rgba(var(--accent-light-rgb), 0.12); +} + .modalButtonSecondary { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.8); diff --git a/webui/src/routes/issues/-ui/issue-detail-modal.tsx b/webui/src/routes/issues/-ui/issue-detail-modal.tsx index 1cbb21d7..c773e8e4 100644 --- a/webui/src/routes/issues/-ui/issue-detail-modal.tsx +++ b/webui/src/routes/issues/-ui/issue-detail-modal.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery } from '@tanstack/react-query'; +import { Link } from '@tanstack/react-router'; import { useEffect, useMemo, useState, type ReactNode } from 'react'; import { DialogBody, DialogFooter, DialogFrame, DialogHeader } from '@/components/dialog'; @@ -22,6 +23,7 @@ import { ISSUE_CATEGORY_META, parseSnapshot, } from '../-issues.helpers'; +import { Route } from '../route'; import styles from './issue-detail-modal.module.css'; export function IssueDetailModal({ @@ -213,9 +215,14 @@ export function IssueDetailModal({ /> {renderIssueDetailContent()} - + ({ ...prev, issueId: undefined })} + to={Route.fullPath} + > Close - + {issue && ( <> {statusButtons} diff --git a/webui/src/routes/issues/-ui/issues-page.module.css b/webui/src/routes/issues/-ui/issues-page.module.css index 9fc1d269..95ab7faa 100644 --- a/webui/src/routes/issues/-ui/issues-page.module.css +++ b/webui/src/routes/issues/-ui/issues-page.module.css @@ -159,6 +159,7 @@ width: 100%; padding: 14px 16px; text-align: left; + text-decoration: none; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.03); diff --git a/webui/src/routes/issues/-ui/issues-page.tsx b/webui/src/routes/issues/-ui/issues-page.tsx index 52e86f83..2354cde6 100644 --- a/webui/src/routes/issues/-ui/issues-page.tsx +++ b/webui/src/routes/issues/-ui/issues-page.tsx @@ -1,5 +1,5 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useNavigate } from '@tanstack/react-router'; +import { Link, useNavigate } from '@tanstack/react-router'; import { Select } from '@/components/form'; import { Show } from '@/components/primitives'; @@ -73,13 +73,6 @@ function IssueBoard() { ...issueListQueryOptions(profileId, params), }); - const openIssue = (issueId: number) => { - void navigate({ - to: Route.fullPath, - search: (prev) => ({ ...prev, issueId }), - }); - }; - const onCategoryChange = (category: IssuesSearch['category']) => { void navigate({ to: Route.fullPath, @@ -112,7 +105,6 @@ function IssueBoard() { issuesError={issuesQuery.error} issuesLoading={issuesQuery.isLoading} showReporterName={isAdmin} - onIssueSelect={openIssue} statusFilter={params.status} /> @@ -225,7 +217,6 @@ function IssueBoardList({ issues, issuesError, issuesLoading, - onIssueSelect, showReporterName, statusFilter, }: { @@ -233,7 +224,6 @@ function IssueBoardList({ issues: IssueRecord[]; issuesError: unknown; issuesLoading: boolean; - onIssueSelect: (issueId: number) => void; showReporterName: boolean; statusFilter: IssuesSearch['status']; }) { @@ -285,7 +275,6 @@ function IssueBoardList({ key={issue.id} issue={issue} showReporterName={showReporterName} - onIssueSelect={onIssueSelect} /> )); } @@ -294,11 +283,9 @@ function IssueBoardList({ function IssueBoardCard({ issue, showReporterName, - onIssueSelect, }: { issue: IssueRecord; showReporterName: boolean; - onIssueSelect: (issueId: number) => void; }) { const snapshot = parseSnapshot(issue.snapshot_data); const artwork = getIssueArtwork(snapshot); @@ -311,11 +298,11 @@ function IssueBoardCard({ const createdDate = formatIssueDate(issue.created_at); return ( - onIssueSelect(issue.id)} + to={Route.fullPath} + search={(prev) => ({ ...prev, issueId: issue.id })} > {artwork ? ( @@ -360,7 +347,7 @@ function IssueBoardCard({ title={`${issue.priority} priority`} /> - + ); }