refactor(issues): use Link to open/close issues
This commit is contained in:
parent
81bdf4355f
commit
c9ac9aeb1a
5 changed files with 67 additions and 26 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
/>
|
||||
<DialogBody>{renderIssueDetailContent()}</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button className={styles.modalButtonSecondary} type="button" onClick={onClose}>
|
||||
<Link
|
||||
className={`${styles.modalLinkButton} ${styles.modalButtonSecondary}`}
|
||||
replace
|
||||
search={(prev) => ({ ...prev, issueId: undefined })}
|
||||
to={Route.fullPath}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Link>
|
||||
{issue && (
|
||||
<>
|
||||
{statusButtons}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -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 (
|
||||
<button
|
||||
<Link
|
||||
className={styles.issueCard}
|
||||
type="button"
|
||||
data-testid={`issue-card-${issue.id}`}
|
||||
onClick={() => onIssueSelect(issue.id)}
|
||||
to={Route.fullPath}
|
||||
search={(prev) => ({ ...prev, issueId: issue.id })}
|
||||
>
|
||||
<div className={styles.issueCardLeft}>
|
||||
{artwork ? (
|
||||
|
|
@ -360,7 +347,7 @@ function IssueBoardCard({
|
|||
title={`${issue.priority} priority`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue