From a4a4c0f12d4082b1d6d12eb6be54e18b7fd9c7a6 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sun, 3 May 2026 12:56:06 +0300 Subject: [PATCH] Flatten issues board rendering - keep the route controller at the top of the file - split the board into small local components - remove the dead close-event helper and keep refresh invalidation only --- webui/src/routes/issues/-issues.helpers.ts | 5 - webui/src/routes/issues/-ui/issues-page.tsx | 623 +++++++++++--------- 2 files changed, 350 insertions(+), 278 deletions(-) diff --git a/webui/src/routes/issues/-issues.helpers.ts b/webui/src/routes/issues/-issues.helpers.ts index 82420ca9..551809ce 100644 --- a/webui/src/routes/issues/-issues.helpers.ts +++ b/webui/src/routes/issues/-issues.helpers.ts @@ -1,7 +1,6 @@ import type { IssueRecord, IssuesSearch, IssueSnapshot } from './-issues.types'; export const REFRESH_EVENT = 'ss:issues-refresh'; -export const CLOSE_EVENT = 'ss:issues-close-detail'; export const DEFAULT_ISSUES_SEARCH = { status: 'open', @@ -126,10 +125,6 @@ export function dispatchIssuesRefreshEvent() { window.dispatchEvent(new CustomEvent(REFRESH_EVENT)); } -export function dispatchIssuesCloseEvent() { - window.dispatchEvent(new CustomEvent(CLOSE_EVENT)); -} - export function parseSnapshot(snapshot: IssueRecord['snapshot_data']): IssueSnapshot { if (!snapshot) { return {}; diff --git a/webui/src/routes/issues/-ui/issues-page.tsx b/webui/src/routes/issues/-ui/issues-page.tsx index 7d5b45e2..4a876198 100644 --- a/webui/src/routes/issues/-ui/issues-page.tsx +++ b/webui/src/routes/issues/-ui/issues-page.tsx @@ -9,15 +9,14 @@ import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types'; import { issueCountsQueryOptions, issueListQueryOptions } from '../-issues.api'; import { - CLOSE_EVENT, REFRESH_EVENT, dispatchIssuesRefreshEvent, + formatIssueDate, getEntityDetails, getEntityLabel, getEntityName, getIssueArtwork, getPriorityClassName, - formatIssueDate, ISSUE_CATEGORY_META, ISSUE_STATUS_META, normalizeIssuesSearch, @@ -29,18 +28,9 @@ import styles from './issues-page.module.css'; export function IssuesPage() { useReactPageShell('issues'); - const { isAdmin, profileId } = useProfile(); - const queryClient = useQueryClient(); const navigate = useNavigate({ from: Route.fullPath }); const params = Route.useSearch(); - const openIssue = (issueId: number) => { - navigate({ - to: Route.fullPath, - search: (prev) => normalizeIssuesSearch({ ...prev, issueId }), - }); - }; - const clearIssueSelection = () => { navigate({ to: Route.fullPath, @@ -49,60 +39,9 @@ export function IssuesPage() { }); }; - useEffect(() => { - const handleRefresh = () => { - queryClient.invalidateQueries({ queryKey: ['issues'] }); - }; - - window.addEventListener(REFRESH_EVENT, handleRefresh); - window.addEventListener(CLOSE_EVENT, clearIssueSelection); - return () => { - window.removeEventListener(REFRESH_EVENT, handleRefresh); - window.removeEventListener(CLOSE_EVENT, clearIssueSelection); - }; - }, [navigate, queryClient]); - - const countsQuery = useQuery({ - ...issueCountsQueryOptions(profileId), - }); - const issuesQuery = useQuery({ - ...issueListQueryOptions(profileId, params), - }); - return ( <> - - navigate({ - to: Route.fullPath, - search: (prev) => - normalizeIssuesSearch({ - ...prev, - category, - }), - replace: true, - }) - } - onIssueSelect={openIssue} - onStatusChange={(status) => - navigate({ - to: Route.fullPath, - search: (prev) => - normalizeIssuesSearch({ - ...prev, - status, - }), - replace: true, - }) - } - statusFilter={params.status} - /> + void; - onIssueSelect: (issueId: number) => void; - onStatusChange: (status: IssueStatus | 'all') => void; - statusFilter: IssueStatus | 'all'; -}) { - const safeCounts = counts ?? { - open: 0, - in_progress: 0, - resolved: 0, - dismissed: 0, - total: 0, +function IssueBoard() { + const { isAdmin, profileId } = useProfile(); + const queryClient = useQueryClient(); + const navigate = useNavigate({ from: Route.fullPath }); + const params = Route.useSearch(); + + useEffect(() => { + const handleRefresh = () => { + queryClient.invalidateQueries({ queryKey: ['issues'] }); + }; + + window.addEventListener(REFRESH_EVENT, handleRefresh); + return () => { + window.removeEventListener(REFRESH_EVENT, handleRefresh); + }; + }, [queryClient]); + + const countsQuery = useQuery({ + ...issueCountsQueryOptions(profileId), + }); + const issuesQuery = useQuery({ + ...issueListQueryOptions(profileId, params), + }); + + const openIssue = (issueId: number) => { + navigate({ + to: Route.fullPath, + search: (prev) => normalizeIssuesSearch({ ...prev, issueId }), + }); + }; + + const onCategoryChange = (category: string) => { + navigate({ + to: Route.fullPath, + search: (prev) => + normalizeIssuesSearch({ + ...prev, + category, + }), + replace: true, + }); + }; + + const onStatusChange = (status: IssueStatus | 'all') => { + navigate({ + to: Route.fullPath, + search: (prev) => + normalizeIssuesSearch({ + ...prev, + status, + }), + replace: true, + }); }; return (
-
-
-

Issues

-

- {isAdmin - ? 'Manage and resolve reported library problems' - : 'Track and resolve library problems'} -

-
-
-
- - -
-
+ + + +
+ ); +} + +function IssueBoardHeader({ + category, + isAdmin, + status, + onCategoryChange, + onStatusChange, +}: { + category: string; + isAdmin: boolean; + status: IssueStatus | 'all'; + onCategoryChange: (category: string) => void; + onStatusChange: (status: IssueStatus | 'all') => void; +}) { + return ( +
+
+

Issues

+

+ {isAdmin + ? 'Manage and resolve reported library problems' + : 'Track and resolve library problems'} +

- -
-
-
{safeCounts.open}
-
Open
+
+
+ +
-
-
{safeCounts.in_progress}
-
In Progress
-
-
-
{safeCounts.resolved}
-
Resolved
-
-
-
{safeCounts.dismissed}
-
Dismissed
-
-
-
{safeCounts.total}
-
Total
-
-
- -
- {issuesLoading ? ( -
-
- Loading issues... -
- ) : issuesError ? ( -
-
Failed to load issues
-
- {issuesError instanceof Error ? issuesError.message : 'Unknown error'} -
-
- ) : issues.length === 0 ? ( -
- -
No issues found
-
- {statusFilter !== 'open' || categoryFilter !== 'all' - ? 'Try adjusting your filters' - : 'No issues have been reported yet'} -
-
- ) : ( - issues.map((issue) => { - const snapshot = parseSnapshot(issue.snapshot_data); - const artwork = getIssueArtwork(snapshot); - const entityName = getEntityName(issue, snapshot); - const details = getEntityDetails(issue, snapshot); - const statusMeta = ISSUE_STATUS_META[issue.status] || ISSUE_STATUS_META.open; - const catMeta = ISSUE_CATEGORY_META[issue.category] || ISSUE_CATEGORY_META.other; - const priorityVariant = getPriorityClassName(issue.priority); - const statusClassName = - issue.status === 'in_progress' - ? styles.issueStatusProgress - : issue.status === 'resolved' - ? styles.issueStatusResolved - : issue.status === 'dismissed' - ? styles.issueStatusDismissed - : styles.issueStatusOpen; - const priorityClass = - priorityVariant === 'high' - ? styles.issuePriorityHigh - : priorityVariant === 'low' - ? styles.issuePriorityLow - : styles.issuePriorityNormal; - const createdDate = formatIssueDate(issue.created_at); - - return ( - - ); - }) - )}
); } + +function IssueBoardStats({ counts }: { counts: IssueCounts }) { + return ( +
+ + + + + +
+ ); + + function IssueStatCard({ + className, + label, + value, + }: { + className: string; + label: string; + value: number; + }) { + return ( +
+
{value}
+
{label}
+
+ ); + } +} + +function IssueBoardList({ + categoryFilter, + issues, + issuesError, + issuesLoading, + onIssueSelect, + showReporterName, + statusFilter, +}: { + categoryFilter: string; + issues: IssueRecord[]; + issuesError: unknown; + issuesLoading: boolean; + onIssueSelect: (issueId: number) => void; + showReporterName: boolean; + statusFilter: IssueStatus | 'all'; +}) { + return ( +
+ +
+ ); + + function IssueBoardListContent() { + if (issuesLoading) { + return ( +
+
+ Loading issues... +
+ ); + } + + if (issuesError) { + return ( +
+
Failed to load issues
+
+ {issuesError instanceof Error ? issuesError.message : 'Unknown error'} +
+
+ ); + } + + if (issues.length === 0) { + return ( +
+ +
No issues found
+
+ {statusFilter !== 'open' || categoryFilter !== 'all' + ? 'Try adjusting your filters' + : 'No issues have been reported yet'} +
+
+ ); + } + + return issues.map((issue) => ( + + )); + } +} + +function IssueBoardCard({ + issue, + showReporterName, + onIssueSelect, +}: { + issue: IssueRecord; + showReporterName: boolean; + onIssueSelect: (issueId: number) => void; +}) { + const snapshot = parseSnapshot(issue.snapshot_data); + const artwork = getIssueArtwork(snapshot); + const entityName = getEntityName(issue, snapshot); + const details = getEntityDetails(issue, snapshot); + const statusMeta = ISSUE_STATUS_META[issue.status] || ISSUE_STATUS_META.open; + const catMeta = ISSUE_CATEGORY_META[issue.category] || ISSUE_CATEGORY_META.other; + const priorityClass = getIssuePriorityClassName(getPriorityClassName(issue.priority)); + const statusClassName = getIssueStatusClassName(issue.status); + const createdDate = formatIssueDate(issue.created_at); + + return ( + + ); +} + +const EMPTY_ISSUE_COUNTS: IssueCounts = { + open: 0, + in_progress: 0, + resolved: 0, + dismissed: 0, + total: 0, +}; + +const ISSUE_STATUS_CLASS_NAMES: Record = { + open: styles.issueStatusOpen, + in_progress: styles.issueStatusProgress, + resolved: styles.issueStatusResolved, + dismissed: styles.issueStatusDismissed, +}; + +const ISSUE_PRIORITY_CLASS_NAMES: Record<'high' | 'low' | 'normal', string> = { + high: styles.issuePriorityHigh, + low: styles.issuePriorityLow, + normal: styles.issuePriorityNormal, +}; + +function getIssueStatusClassName(status: IssueRecord['status']): string { + return ISSUE_STATUS_CLASS_NAMES[status] || styles.issueStatusOpen; +} + +function getIssuePriorityClassName(priority: 'high' | 'low' | 'normal'): string { + return ISSUE_PRIORITY_CLASS_NAMES[priority] || styles.issuePriorityNormal; +}