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
This commit is contained in:
Antti Kettunen 2026-05-03 13:19:45 +03:00
parent a4a4c0f12d
commit adb6426a2f
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
5 changed files with 98 additions and 34 deletions

View file

@ -0,0 +1 @@
export { Show } from './show';

View file

@ -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(
<Show when={true}>
<span>Visible</span>
</Show>,
);
expect(screen.getByText('Visible')).toBeInTheDocument();
});
it('renders fallback when the condition is false', () => {
render(
<Show fallback={<span>Hidden</span>} when={false}>
<span>Visible</span>
</Show>,
);
expect(screen.getByText('Hidden')).toBeInTheDocument();
expect(screen.queryByText('Visible')).not.toBeInTheDocument();
});
it('supports render-prop children', () => {
render(<Show when="Ada">{(name) => <span>{name}</span>}</Show>);
expect(screen.getByText('Ada')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,23 @@
import type { ReactNode } from 'react';
type ShowChildren<T> = ReactNode | ((value: NonNullable<T>) => ReactNode);
export function Show<T>({
fallback = null,
children,
when,
}: {
children: ShowChildren<T>;
fallback?: ReactNode;
when: T;
}) {
if (!when) {
return <>{fallback}</>;
}
if (typeof children === 'function') {
return <>{(children as (value: NonNullable<T>) => ReactNode)(when as NonNullable<T>)}</>;
}
return <>{children}</>;
}

View file

@ -14,6 +14,7 @@ import {
TextArea, TextArea,
TextInput, TextInput,
} from '@/components/form'; } from '@/components/form';
import { Show } from '@/components/primitives';
import { useProfile } from '@/platform/shell/route-controllers'; import { useProfile } from '@/platform/shell/route-controllers';
import type { IssuePriority, IssueReportPayload } from '../-issues.types'; import type { IssuePriority, IssueReportPayload } from '../-issues.types';
@ -23,6 +24,7 @@ import {
REFRESH_EVENT, REFRESH_EVENT,
createDefaultIssueTitle, createDefaultIssueTitle,
getIssueCategoriesForEntity, getIssueCategoriesForEntity,
getEntityLabel,
} from '../-issues.helpers'; } from '../-issues.helpers';
import styles from './issue-detail-modal.module.css'; import styles from './issue-detail-modal.module.css';
@ -89,19 +91,21 @@ export function IssueDomainHost() {
}; };
}, [queryClient]); }, [queryClient]);
if (!reportPayload) return null;
return ( return (
<ReportIssueModal <Show when={reportPayload}>
key={`${reportPayload.entityType}:${reportPayload.entityId}`} {(payload) => (
payload={reportPayload} <ReportIssueModal
profileId={profileId} key={`${payload.entityType}:${payload.entityId}`}
onClose={() => setReportPayload(null)} payload={payload}
onSubmitted={() => { profileId={profileId}
setReportPayload(null); onClose={() => setReportPayload(null)}
void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY }); onSubmitted={() => {
}} setReportPayload(null);
/> void queryClient.invalidateQueries({ queryKey: ISSUE_DOMAIN_QUERY_KEY });
}}
/>
)}
</Show>
); );
} }
@ -120,8 +124,6 @@ function ReportIssueModal({
() => getIssueCategoriesForEntity(payload.entityType), () => getIssueCategoriesForEntity(payload.entityType),
[payload.entityType], [payload.entityType],
); );
const entityLabel =
payload.entityType === 'track' ? 'Track' : payload.entityType === 'album' ? 'Album' : 'Artist';
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (values: ReportIssueFormValues) => { mutationFn: async (values: ReportIssueFormValues) => {
@ -172,9 +174,11 @@ function ReportIssueModal({
} }
}} }}
className={styles.reportIssueDialog} className={styles.reportIssueDialog}
closeLabel="Close report issue modal"
> >
<DialogHeader title={`Report Issue - ${entityLabel}`} closeLabel="Close report issue modal" /> <DialogHeader
title={`Report Issue - ${getEntityLabel(payload.entityType)}`}
closeLabel="Close report issue modal"
/>
<DialogBody> <DialogBody>
<form <form
className={styles.reportIssueForm} className={styles.reportIssueForm}
@ -186,12 +190,14 @@ function ReportIssueModal({
> >
<div className={styles.reportIssueEntityInfo}> <div className={styles.reportIssueEntityInfo}>
<div className={styles.reportIssueEntityName}>{payload.entityName}</div> <div className={styles.reportIssueEntityName}>{payload.entityName}</div>
{payload.artistName ? ( <Show when={payload.artistName}>
<div className={styles.reportIssueEntityArtist}> {(artistName) => (
{payload.artistName} <div className={styles.reportIssueEntityArtist}>
{payload.albumTitle ? ` - ${payload.albumTitle}` : ''} {artistName}
</div> <Show when={payload.albumTitle}>{(albumTitle) => ` - ${albumTitle}`}</Show>
) : null} </div>
)}
</Show>
</div> </div>
<FormField <FormField
@ -228,8 +234,8 @@ function ReportIssueModal({
</FormField> </FormField>
<form.Subscribe selector={(state) => state.values.category}> <form.Subscribe selector={(state) => state.values.category}>
{(selectedCategory) => {(selectedCategory) => (
selectedCategory ? ( <Show when={selectedCategory}>
<> <>
<form.Field name="title"> <form.Field name="title">
{(field) => ( {(field) => (
@ -296,8 +302,8 @@ function ReportIssueModal({
)} )}
</form.Field> </form.Field>
</> </>
) : null </Show>
} )}
</form.Subscribe> </form.Subscribe>
<form.Subscribe selector={(state) => state.errors}> <form.Subscribe selector={(state) => state.errors}>

View file

@ -3,6 +3,7 @@ import { useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Select } from '@/components/form'; import { Select } from '@/components/form';
import { Show } from '@/components/primitives';
import { useProfile, useReactPageShell } from '@/platform/shell/route-controllers'; import { useProfile, useReactPageShell } from '@/platform/shell/route-controllers';
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types'; import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
@ -350,27 +351,27 @@ function IssueBoardCard({
{catMeta.icon} {catMeta.icon}
</span> </span>
<span className={styles.issueCardTitle}>{issue.title}</span> <span className={styles.issueCardTitle}>{issue.title}</span>
{issue.admin_response ? ( <Show when={issue.admin_response}>
<span className={styles.issueCardResponded} title="Admin has responded"> <span className={styles.issueCardResponded} title="Admin has responded">
💬 💬
</span> </span>
) : null} </Show>
</div> </div>
<div className={styles.issueCardEntity}> <div className={styles.issueCardEntity}>
<span className={styles.issueCardEntityType}>{getEntityLabel(issue.entity_type)}</span> <span className={styles.issueCardEntityType}>{getEntityLabel(issue.entity_type)}</span>
<span className={styles.issueCardEntityName}>{entityName}</span> <span className={styles.issueCardEntityName}>{entityName}</span>
{details.length > 0 ? ( <Show when={details.length}>
<span className={styles.issueCardMetaLine}>{details.join(' - ')}</span> <span className={styles.issueCardMetaLine}>{details.join(' - ')}</span>
) : null} </Show>
</div> </div>
{issue.description ? ( <Show when={issue.description}>
<div className={styles.issueCardDescription}>{issue.description}</div> <div className={styles.issueCardDescription}>{issue.description}</div>
) : null} </Show>
<div className={styles.issueCardFooter}> <div className={styles.issueCardFooter}>
<span className={styles.issueCardDate}>{createdDate}</span> <span className={styles.issueCardDate}>{createdDate}</span>
{showReporterName && issue.reporter_name ? ( <Show when={showReporterName && issue.reporter_name}>
<span className={styles.issueCardProfile}>by {issue.reporter_name}</span> <span className={styles.issueCardProfile}>by {issue.reporter_name}</span>
) : null} </Show>
</div> </div>
</div> </div>
<div className={styles.issueCardRight}> <div className={styles.issueCardRight}>