From adb6426a2f19e051579916a0aca31f2017525a1a Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sun, 3 May 2026 13:19:45 +0300 Subject: [PATCH] Add shared Show primitive - move the conditional rendering helper into components/primitives - use it in the issues board and issue domain host - keep the issue page and host easier to scan without repeated null branches --- webui/src/components/primitives/index.ts | 1 + webui/src/components/primitives/show.test.tsx | 33 +++++++++++ webui/src/components/primitives/show.tsx | 23 ++++++++ .../routes/issues/-ui/issue-domain-host.tsx | 58 ++++++++++--------- webui/src/routes/issues/-ui/issues-page.tsx | 17 +++--- 5 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 webui/src/components/primitives/index.ts create mode 100644 webui/src/components/primitives/show.test.tsx create mode 100644 webui/src/components/primitives/show.tsx diff --git a/webui/src/components/primitives/index.ts b/webui/src/components/primitives/index.ts new file mode 100644 index 00000000..849c5bcc --- /dev/null +++ b/webui/src/components/primitives/index.ts @@ -0,0 +1 @@ +export { Show } from './show'; diff --git a/webui/src/components/primitives/show.test.tsx b/webui/src/components/primitives/show.test.tsx new file mode 100644 index 00000000..ec090d2c --- /dev/null +++ b/webui/src/components/primitives/show.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { Show } from './show'; + +describe('Show', () => { + it('renders children when the condition is true', () => { + render( + + Visible + , + ); + + expect(screen.getByText('Visible')).toBeInTheDocument(); + }); + + it('renders fallback when the condition is false', () => { + render( + Hidden} when={false}> + Visible + , + ); + + expect(screen.getByText('Hidden')).toBeInTheDocument(); + expect(screen.queryByText('Visible')).not.toBeInTheDocument(); + }); + + it('supports render-prop children', () => { + render({(name) => {name}}); + + expect(screen.getByText('Ada')).toBeInTheDocument(); + }); +}); diff --git a/webui/src/components/primitives/show.tsx b/webui/src/components/primitives/show.tsx new file mode 100644 index 00000000..2265131a --- /dev/null +++ b/webui/src/components/primitives/show.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from 'react'; + +type ShowChildren = ReactNode | ((value: NonNullable) => ReactNode); + +export function Show({ + fallback = null, + children, + when, +}: { + children: ShowChildren; + fallback?: ReactNode; + when: T; +}) { + if (!when) { + return <>{fallback}; + } + + if (typeof children === 'function') { + return <>{(children as (value: NonNullable) => ReactNode)(when as NonNullable)}; + } + + return <>{children}; +} diff --git a/webui/src/routes/issues/-ui/issue-domain-host.tsx b/webui/src/routes/issues/-ui/issue-domain-host.tsx index 8aac7d05..49f29f04 100644 --- a/webui/src/routes/issues/-ui/issue-domain-host.tsx +++ b/webui/src/routes/issues/-ui/issue-domain-host.tsx @@ -14,6 +14,7 @@ import { TextArea, TextInput, } from '@/components/form'; +import { Show } from '@/components/primitives'; import { useProfile } from '@/platform/shell/route-controllers'; import type { IssuePriority, IssueReportPayload } from '../-issues.types'; @@ -23,6 +24,7 @@ import { REFRESH_EVENT, createDefaultIssueTitle, getIssueCategoriesForEntity, + getEntityLabel, } from '../-issues.helpers'; import styles from './issue-detail-modal.module.css'; @@ -89,19 +91,21 @@ export function IssueDomainHost() { }; }, [queryClient]); - if (!reportPayload) return null; - return ( - setReportPayload(null)} - onSubmitted={() => { - setReportPayload(null); - void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY }); - }} - /> + + {(payload) => ( + setReportPayload(null)} + onSubmitted={() => { + setReportPayload(null); + void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY }); + }} + /> + )} + ); } @@ -120,8 +124,6 @@ function ReportIssueModal({ () => getIssueCategoriesForEntity(payload.entityType), [payload.entityType], ); - const entityLabel = - payload.entityType === 'track' ? 'Track' : payload.entityType === 'album' ? 'Album' : 'Artist'; const createMutation = useMutation({ mutationFn: async (values: ReportIssueFormValues) => { @@ -172,9 +174,11 @@ function ReportIssueModal({ } }} className={styles.reportIssueDialog} - closeLabel="Close report issue modal" > - +
{payload.entityName}
- {payload.artistName ? ( -
- {payload.artistName} - {payload.albumTitle ? ` - ${payload.albumTitle}` : ''} -
- ) : null} + + {(artistName) => ( +
+ {artistName} + {(albumTitle) => ` - ${albumTitle}`} +
+ )} +
state.values.category}> - {(selectedCategory) => - selectedCategory ? ( + {(selectedCategory) => ( + <> {(field) => ( @@ -296,8 +302,8 @@ function ReportIssueModal({ )} - ) : null - } + + )} state.errors}> diff --git a/webui/src/routes/issues/-ui/issues-page.tsx b/webui/src/routes/issues/-ui/issues-page.tsx index 4a876198..8c41be88 100644 --- a/webui/src/routes/issues/-ui/issues-page.tsx +++ b/webui/src/routes/issues/-ui/issues-page.tsx @@ -3,6 +3,7 @@ import { useNavigate } from '@tanstack/react-router'; import { useEffect } from 'react'; import { Select } from '@/components/form'; +import { Show } from '@/components/primitives'; import { useProfile, useReactPageShell } from '@/platform/shell/route-controllers'; import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types'; @@ -350,27 +351,27 @@ function IssueBoardCard({ {catMeta.icon} {issue.title} - {issue.admin_response ? ( + 💬 - ) : null} +
{getEntityLabel(issue.entity_type)} {entityName} - {details.length > 0 ? ( + {details.join(' - ')} - ) : null} +
- {issue.description ? ( +
{issue.description}
- ) : null} +
{createdDate} - {showReporterName && issue.reporter_name ? ( + by {issue.reporter_name} - ) : null} +